/**
* 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;
}