/** * 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 | 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, options); } return item; }) as unknown as T; } // Handle objects const sanitized: Record = {}; 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, 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( 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, options); } return item; }); return result as T; }