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]>
124 lines
3.3 KiB
TypeScript
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;
|
|
}
|