From 01639fff957e204177cab4b79ef0645640566a99 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 3 Feb 2026 21:47:32 -0600 Subject: [PATCH] feat(#285): Add input sanitization for XSS prevention 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 --- .../common/decorators/sanitize.decorator.ts | 52 ++++ .../src/common/utils/sanitize.util.spec.ts | 171 +++++++++++++ apps/api/src/common/utils/sanitize.util.ts | 123 ++++++++++ apps/api/src/federation/dto/command.dto.ts | 3 + apps/api/src/federation/dto/connection.dto.ts | 4 + .../federation/dto/identity-linking.dto.ts | 3 + .../dto/sanitization.integration.spec.ts | 225 ++++++++++++++++++ ...r.ts_20260203-2144_1_remediation_needed.md | 20 ++ ...r.ts_20260203-2145_1_remediation_needed.md | 20 ++ ...r.ts_20260203-2145_2_remediation_needed.md | 20 ++ ...r.ts_20260203-2146_1_remediation_needed.md | 20 ++ ...c.ts_20260203-2143_1_remediation_needed.md | 20 ++ ...l.ts_20260203-2144_1_remediation_needed.md | 20 ++ ...l.ts_20260203-2146_1_remediation_needed.md | 20 ++ ...l.ts_20260203-2146_2_remediation_needed.md | 20 ++ ...l.ts_20260203-2147_1_remediation_needed.md | 20 ++ ...l.ts_20260203-2147_2_remediation_needed.md | 20 ++ ...o.ts_20260203-2144_1_remediation_needed.md | 20 ++ ...o.ts_20260203-2144_2_remediation_needed.md | 20 ++ ...o.ts_20260203-2144_1_remediation_needed.md | 20 ++ ...o.ts_20260203-2144_2_remediation_needed.md | 20 ++ ...o.ts_20260203-2144_1_remediation_needed.md | 20 ++ ...o.ts_20260203-2144_2_remediation_needed.md | 20 ++ ...c.ts_20260203-2145_1_remediation_needed.md | 20 ++ 24 files changed, 921 insertions(+) create mode 100644 apps/api/src/common/decorators/sanitize.decorator.ts create mode 100644 apps/api/src/common/utils/sanitize.util.spec.ts create mode 100644 apps/api/src/common/utils/sanitize.util.ts create mode 100644 apps/api/src/federation/dto/sanitization.integration.spec.ts create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-decorators-sanitize.decorator.ts_20260203-2144_1_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-decorators-sanitize.decorator.ts_20260203-2145_1_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-decorators-sanitize.decorator.ts_20260203-2145_2_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-decorators-sanitize.decorator.ts_20260203-2146_1_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-sanitize.util.spec.ts_20260203-2143_1_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-sanitize.util.ts_20260203-2144_1_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-sanitize.util.ts_20260203-2146_1_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-sanitize.util.ts_20260203-2146_2_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-sanitize.util.ts_20260203-2147_1_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-sanitize.util.ts_20260203-2147_2_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-dto-command.dto.ts_20260203-2144_1_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-dto-command.dto.ts_20260203-2144_2_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-dto-connection.dto.ts_20260203-2144_1_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-dto-connection.dto.ts_20260203-2144_2_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-dto-identity-linking.dto.ts_20260203-2144_1_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-dto-identity-linking.dto.ts_20260203-2144_2_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-dto-sanitization.integration.spec.ts_20260203-2145_1_remediation_needed.md 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("