Files
stack/apps/api/src/common/utils/sanitize.util.ts
T
jason.woltjeandClaude Sonnet 4.5 01639fff95 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 <[email protected]>
2026-02-03 21:47:32 -06:00

124 lines
3.3 KiB
TypeScript

/**
* Sanitization Utilities
*
* Provides HTML/XSS sanitization for user-controlled input.
* Uses sanitize-html to prevent XSS attacks.
*/
import sanitizeHtml from "sanitize-html";
/**
* Sanitize options for strict mode (default)
* Allows only safe tags and attributes, removes all scripts and dangerous content
*/
const STRICT_OPTIONS: sanitizeHtml.IOptions = {
allowedTags: ["p", "b", "i", "em", "strong", "a", "br", "ul", "ol", "li"],
allowedAttributes: {
a: ["href"],
},
allowedSchemes: ["http", "https", "mailto"],
disallowedTagsMode: "discard",
};
/**
* Sanitize a string value to prevent XSS attacks
* Removes dangerous HTML tags, scripts, and event handlers
*
* @param value - String to sanitize
* @param options - Optional sanitize-html options (defaults to strict)
* @returns Sanitized string
*/
export function sanitizeString(
value: string | null | undefined,
options: sanitizeHtml.IOptions = STRICT_OPTIONS
): string {
if (value === null || value === undefined) {
return "";
}
// Convert non-strings to strings
const stringValue = typeof value === "string" ? value : String(value);
return sanitizeHtml(stringValue, options);
}
/**
* Sanitize all string values in an object recursively
* Preserves object structure and non-string values
*
* @param obj - Object to sanitize
* @param options - Optional sanitize-html options
* @returns Sanitized object
*/
export function sanitizeObject<T extends Record<string, unknown> | null | undefined>(
obj: T,
options: sanitizeHtml.IOptions = STRICT_OPTIONS
): T {
// Handle null/undefined
if (obj == null) {
return obj;
}
// Handle arrays
if (Array.isArray(obj)) {
return obj.map((item: unknown) => {
if (typeof item === "string") {
return sanitizeString(item, options);
}
if (typeof item === "object" && item !== null) {
return sanitizeObject(item as Record<string, unknown>, options);
}
return item;
}) as unknown as T;
}
// Handle objects
const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
if (typeof value === "string") {
sanitized[key] = sanitizeString(value, options);
} else if (Array.isArray(value)) {
sanitized[key] = sanitizeArray(value, options);
} else if (typeof value === "object" && value !== null) {
sanitized[key] = sanitizeObject(value as Record<string, unknown>, options);
} else {
sanitized[key] = value;
}
}
return sanitized as T;
}
/**
* Sanitize all string values in an array recursively
* Preserves array structure and non-string values
*
* @param arr - Array to sanitize
* @param options - Optional sanitize-html options
* @returns Sanitized array
*/
export function sanitizeArray<T extends unknown[] = unknown[]>(
arr: T,
options: sanitizeHtml.IOptions = STRICT_OPTIONS
): T {
if (!Array.isArray(arr)) {
return arr;
}
const result = arr.map((item: unknown) => {
if (typeof item === "string") {
return sanitizeString(item, options);
}
if (Array.isArray(item)) {
return sanitizeArray(item as unknown[], options);
}
if (typeof item === "object" && item !== null) {
return sanitizeObject(item as Record<string, unknown>, options);
}
return item;
});
return result as T;
}