diff --git a/apps/api/src/common/decorators/sanitize.decorator.ts b/apps/api/src/common/decorators/sanitize.decorator.ts new file mode 100644 index 0000000..4819387 --- /dev/null +++ b/apps/api/src/common/decorators/sanitize.decorator.ts @@ -0,0 +1,52 @@ +/** + * Sanitize Decorator + * + * Custom class-validator decorator to sanitize string input and prevent XSS. + */ + +import { Transform } from "class-transformer"; +import { sanitizeString, sanitizeObject } from "../utils/sanitize.util"; + +/** + * Sanitize decorator for DTO properties + * Automatically sanitizes string values to prevent XSS attacks + * + * Usage: + * ```typescript + * class MyDto { + * @Sanitize() + * @IsString() + * userInput!: string; + * } + * ``` + */ +export function Sanitize(): PropertyDecorator { + return Transform(({ value }: { value: unknown }) => { + if (typeof value === "string") { + return sanitizeString(value); + } + return value; + }); +} + +/** + * SanitizeObject decorator for nested objects + * Recursively sanitizes all string values in an object + * + * Usage: + * ```typescript + * class MyDto { + * @SanitizeObject() + * @IsObject() + * metadata?: Record; + * } + * ``` + */ +export function SanitizeObject(): PropertyDecorator { + return Transform(({ value }: { value: unknown }) => { + if (typeof value === "object" && value !== null) { + return sanitizeObject(value as Record); + } + return value; + }); +} diff --git a/apps/api/src/common/utils/sanitize.util.spec.ts b/apps/api/src/common/utils/sanitize.util.spec.ts new file mode 100644 index 0000000..c181ce5 --- /dev/null +++ b/apps/api/src/common/utils/sanitize.util.spec.ts @@ -0,0 +1,171 @@ +/** + * Sanitization Utility Tests + * + * Tests for HTML sanitization and XSS prevention. + */ + +import { describe, it, expect } from "vitest"; +import { sanitizeString, sanitizeObject, sanitizeArray } from "./sanitize.util"; + +describe("Sanitization Utilities", () => { + describe("sanitizeString", () => { + it("should remove script tags", () => { + const dirty = 'Hello'; + const clean = sanitizeString(dirty); + + expect(clean).not.toContain("John', + description: "Safe text", + nested: { + value: '', + }, + }; + + const clean = sanitizeObject(dirty); + + expect(clean.name).not.toContain("safe", "another", + }, + }, + }, + }; + + const clean = sanitizeObject(input); + + expect(clean.level1.level2.level3.xss).not.toContain("safe", "clean", '']; + + const clean = sanitizeArray(dirty); + + expect(clean[0]).not.toContain("", 123, true, null, { key: "value" }]; + + const clean = sanitizeArray(input); + + expect(clean[0]).not.toContain("", "safe"], ['']]; + + const clean = sanitizeArray(input); + + expect(clean[0][0]).not.toContain("Connection rejected', + }; + + const dto = plainToInstance(RejectConnectionDto, dirty); + const errors = await validate(dto); + + expect(errors).toHaveLength(0); + expect(dto.reason).not.toContain("Important", + nested: { + value: "", + }, + }, + }; + + const dto = plainToInstance(AcceptConnectionDto, dirty); + const errors = await validate(dto); + + expect(errors).toHaveLength(0); + expect(dto.metadata!.note).not.toContain("John Doe', + bio: 'Developer', + }, + }; + + const dto = plainToInstance(CreateIdentityMappingDto, dirty); + const errors = await validate(dto); + + expect(errors).toHaveLength(0); + expect(dto.metadata!.displayName).not.toContain("", "tag2"], + }, + }; + + const dto = plainToInstance(UpdateIdentityMappingDto, dirty); + const errors = await validate(dto); + + expect(errors).toHaveLength(0); + expect((dto.metadata!.tags as any)[0]).not.toContain("console.log("hello")', + params: { + arg1: '', + }, + }, + }; + + const dto = plainToInstance(SendCommandDto, dirty); + const errors = await validate(dto); + + expect(errors).toHaveLength(0); + expect(dto.payload.script).not.toContain("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("', + }, + }; + + const dto = plainToInstance(AcceptConnectionDto, dirty); + + expect(dto.metadata!.style).not.toContain("