sanitization.ts•30.7 kB
/**
* @fileoverview Provides a comprehensive sanitization utility class for various input types,
* including HTML, strings, URLs, file paths, JSON, and numbers. It also includes
* functionality for redacting sensitive information from objects for safe logging.
* @module src/utils/security/sanitization
*/
import path from "path";
import sanitizeHtml from "sanitize-html";
import validator from "validator";
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
import {
logger,
RequestContext,
requestContextService,
} from "../internal/index.js"; // Use internal index
/**
* Options for path sanitization, controlling how file paths are cleaned and validated.
*/
export interface PathSanitizeOptions {
/**
* If provided, restricts sanitized paths to be relative to this root directory.
* Attempts to traverse above this root (e.g., using `../`) will result in an error.
* The final sanitized path will be relative to this `rootDir`.
*/
rootDir?: string;
/**
* If `true`, normalizes Windows-style backslashes (`\\`) to POSIX-style forward slashes (`/`).
* Defaults to `false`.
*/
toPosix?: boolean;
/**
* If `true`, allows absolute paths, subject to `rootDir` constraints if `rootDir` is also provided.
* If `false` (default), absolute paths are converted to relative paths by removing leading slashes or drive letters.
*/
allowAbsolute?: boolean;
}
/**
* Information returned by the `sanitizePath` method, providing details about
* the sanitization process and its outcome.
*/
export interface SanitizedPathInfo {
/** The final sanitized and normalized path string. */
sanitizedPath: string;
/** The original path string passed to the function before any normalization or sanitization. */
originalInput: string;
/** Indicates if the input path was determined to be absolute after initial `path.normalize()`. */
wasAbsolute: boolean;
/**
* Indicates if an initially absolute path was converted to a relative path
* (typically because `options.allowAbsolute` was `false`).
*/
convertedToRelative: boolean;
/** The effective options (including defaults) that were used for sanitization. */
optionsUsed: PathSanitizeOptions;
}
/**
* Options for context-specific string sanitization using `sanitizeString`.
*/
export interface SanitizeStringOptions {
/**
* Specifies the context in which the string will be used, guiding the sanitization strategy.
* - `'text'`: (Default) Strips all HTML tags, suitable for plain text content.
* - `'html'`: Sanitizes for safe HTML embedding, using `allowedTags` and `allowedAttributes`.
* - `'attribute'`: Sanitizes for use within an HTML attribute value (strips all tags).
* - `'url'`: Validates and trims the string as a URL.
* - `'javascript'`: **Disallowed.** Throws an error to prevent unsafe JavaScript sanitization.
*/
context?: "text" | "html" | "attribute" | "url" | "javascript";
/** Custom allowed HTML tags when `context` is `'html'`. Overrides default HTML sanitization tags. */
allowedTags?: string[];
/** Custom allowed HTML attributes per tag when `context` is `'html'`. Overrides default HTML sanitization attributes. */
allowedAttributes?: Record<string, string[]>;
}
/**
* Configuration options for HTML sanitization using `sanitizeHtml`.
*/
export interface HtmlSanitizeConfig {
/** An array of allowed HTML tag names (e.g., `['p', 'a', 'strong']`). */
allowedTags?: string[];
/**
* A map specifying allowed attributes for HTML tags.
* Keys can be tag names (e.g., `'a'`) or `'*'` for global attributes.
* Values are arrays of allowed attribute names (e.g., `{'a': ['href', 'title']}`).
*/
allowedAttributes?: sanitizeHtml.IOptions["allowedAttributes"];
/** If `true`, HTML comments (`<!-- ... -->`) are preserved. Defaults to `false`. */
preserveComments?: boolean;
/**
* Custom rules for transforming tags during sanitization.
* See `sanitize-html` documentation for `transformTags` options.
*/
transformTags?: sanitizeHtml.IOptions["transformTags"];
}
/**
* A singleton utility class for performing various input sanitization tasks.
* It provides methods to clean and validate strings, HTML, URLs, file paths, JSON,
* and numbers, and to redact sensitive data for logging.
*/
export class Sanitization {
private static instance: Sanitization;
private sensitiveFields: string[] = [
"password",
"token",
"secret",
"key",
"apiKey",
"auth",
"credential",
"jwt",
"ssn",
"credit",
"card",
"cvv",
"authorization",
"passphrase",
"privatekey", // Added more common sensitive field names
"obsidianapikey", // Specific to this project potentially
];
private defaultHtmlSanitizeConfig: HtmlSanitizeConfig = {
allowedTags: [
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"p",
"a",
"ul",
"ol",
"li",
"b",
"i",
"strong",
"em",
"strike",
"code",
"hr",
"br",
"div",
"table",
"thead",
"tbody",
"tr",
"th",
"td",
"pre",
"blockquote", // Added blockquote
],
allowedAttributes: {
a: ["href", "name", "target", "title"], // Added title for links
img: ["src", "alt", "title", "width", "height"],
"*": ["class", "id", "style", "data-*"], // Allow data-* attributes
},
preserveComments: false,
};
private constructor() {
// Singleton constructor
}
/**
* Gets the singleton instance of the `Sanitization` class.
* @returns {Sanitization} The singleton instance.
*/
public static getInstance(): Sanitization {
if (!Sanitization.instance) {
Sanitization.instance = new Sanitization();
}
return Sanitization.instance;
}
/**
* Sets or extends the list of field names considered sensitive for log redaction.
* Field names are matched case-insensitively.
* @param {string[]} fields - An array of field names to add to the sensitive list.
* @param {RequestContext} [context] - Optional context for logging this configuration change.
*/
public setSensitiveFields(fields: string[], context?: RequestContext): void {
const opContext =
context ||
requestContextService.createRequestContext({
operation: "Sanitization.setSensitiveFields",
});
this.sensitiveFields = [
...new Set([
...this.sensitiveFields,
...fields.map((f) => f.toLowerCase()),
]),
];
logger.debug("Updated sensitive fields list for log redaction.", {
...opContext,
newCount: this.sensitiveFields.length,
});
}
/**
* Retrieves a copy of the current list of sensitive field names used for log redaction.
* @returns {string[]} An array of sensitive field names (all lowercase).
*/
public getSensitiveFields(): string[] {
return [...this.sensitiveFields];
}
/**
* Sanitizes an HTML string by removing potentially malicious tags and attributes,
* based on a configurable allow-list.
* @param {string} input - The HTML string to sanitize.
* @param {HtmlSanitizeConfig} [config] - Optional custom configuration for HTML sanitization.
* Overrides defaults for `allowedTags`, `allowedAttributes`, etc.
* @returns {string} The sanitized HTML string. Returns an empty string if input is falsy.
*/
public sanitizeHtml(input: string, config?: HtmlSanitizeConfig): string {
if (!input) return "";
const effectiveConfig = { ...this.defaultHtmlSanitizeConfig, ...config };
const options: sanitizeHtml.IOptions = {
allowedTags: effectiveConfig.allowedTags,
allowedAttributes: effectiveConfig.allowedAttributes,
transformTags: effectiveConfig.transformTags,
};
if (effectiveConfig.preserveComments) {
// Ensure '!--' is not duplicated if already present
options.allowedTags = [
...new Set([...(options.allowedTags || []), "!--"]),
];
}
return sanitizeHtml(input, options);
}
/**
* Sanitizes a tag name by removing the leading '#' and replacing invalid characters.
* @param {string} input - The tag string to sanitize.
* @returns {string} The sanitized tag name.
*/
public sanitizeTagName(input: string): string {
if (!input) return "";
// Remove leading '#' and replace spaces/invalid characters with nothing
return input.replace(/^#/, "").replace(/[\s#,\\?%*:|"<>]/g, "");
}
/**
>>>>>>> REPLACE
* Sanitizes a string based on its intended usage context (e.g., HTML, URL, plain text).
*
* **Security Note:** Using `context: 'javascript'` is explicitly disallowed and will throw an `McpError`.
* This is to prevent accidental introduction of XSS vulnerabilities through ineffective sanitization
* of JavaScript code. Proper contextual encoding or safer methods should be used for JavaScript.
*
* @param {string} input - The string to sanitize.
* @param {SanitizeStringOptions} [options={}] - Options specifying the sanitization context
* and any context-specific parameters (like `allowedTags` for HTML).
* @param {RequestContext} [contextForLogging] - Optional context for logging warnings or errors.
* @returns {string} The sanitized string. Returns an empty string if input is falsy.
* @throws {McpError} If `options.context` is `'javascript'`.
*/
public sanitizeString(
input: string,
options: SanitizeStringOptions = {},
contextForLogging?: RequestContext,
): string {
const opContext =
contextForLogging ||
requestContextService.createRequestContext({
operation: "sanitizeString",
inputContext: options.context,
});
if (!input) return "";
switch (options.context) {
case "html":
return this.sanitizeHtml(input, {
allowedTags: options.allowedTags,
allowedAttributes: options.allowedAttributes
? this.convertAttributesFormat(options.allowedAttributes)
: undefined,
});
case "attribute":
// For HTML attributes, strip all tags. Values should be further encoded by the templating engine.
return sanitizeHtml(input, { allowedTags: [], allowedAttributes: {} });
case "url":
// Validate and trim. Throws McpError on failure.
try {
return this.sanitizeUrl(input, ["http", "https"], opContext); // Use sanitizeUrl for consistent validation
} catch (urlError) {
logger.warning(
"Invalid URL detected during string sanitization (context: url).",
{
...opContext,
input,
error:
urlError instanceof Error ? urlError.message : String(urlError),
},
);
return ""; // Return empty or rethrow, depending on desired strictness. Empty for now.
}
case "javascript":
logger.error(
"Attempted JavaScript sanitization via sanitizeString, which is disallowed.",
{ ...opContext, inputPreview: input.substring(0, 100) },
);
throw new McpError(
BaseErrorCode.VALIDATION_ERROR,
"JavaScript sanitization is not supported via sanitizeString due to security risks. Use appropriate contextual encoding or safer alternatives.",
opContext,
);
case "text":
default:
// Default to stripping all HTML for plain text contexts.
return sanitizeHtml(input, { allowedTags: [], allowedAttributes: {} });
}
}
/**
* Sanitizes a URL string by validating its format and protocol.
* @param {string} input - The URL string to sanitize.
* @param {string[]} [allowedProtocols=['http', 'https']] - An array of allowed URL protocols (e.g., 'http', 'https', 'ftp').
* @param {RequestContext} [contextForLogging] - Optional context for logging errors.
* @returns {string} The sanitized and trimmed URL string.
* @throws {McpError} If the URL is invalid, uses a disallowed protocol, or contains 'javascript:'.
*/
public sanitizeUrl(
input: string,
allowedProtocols: string[] = ["http", "https"],
contextForLogging?: RequestContext,
): string {
const opContext =
contextForLogging ||
requestContextService.createRequestContext({ operation: "sanitizeUrl" });
try {
if (!input || typeof input !== "string") {
throw new Error("Invalid URL input: must be a non-empty string.");
}
const trimmedInput = input.trim();
// Stricter check for 'javascript:' regardless of validator's protocol check
if (trimmedInput.toLowerCase().startsWith("javascript:")) {
throw new Error("JavaScript pseudo-protocol is explicitly disallowed.");
}
if (
!validator.isURL(trimmedInput, {
protocols: allowedProtocols,
require_protocol: true,
})
) {
throw new Error(
`Invalid URL format or protocol not in allowed list: [${allowedProtocols.join(", ")}].`,
);
}
return trimmedInput;
} catch (error) {
const message =
error instanceof Error ? error.message : "Invalid or disallowed URL.";
logger.warning(`URL sanitization failed: ${message}`, {
...opContext,
input,
});
throw new McpError(BaseErrorCode.VALIDATION_ERROR, message, {
...opContext,
input,
});
}
}
/**
* Sanitizes a file path to prevent path traversal attacks and normalize its format.
*
* @param {string} input - The file path string to sanitize.
* @param {PathSanitizeOptions} [options={}] - Options to control sanitization behavior (e.g., `rootDir`, `toPosix`).
* @param {RequestContext} [contextForLogging] - Optional context for logging warnings or errors.
* @returns {SanitizedPathInfo} An object containing the sanitized path and metadata about the sanitization.
* @throws {McpError} If the path is invalid (e.g., empty, contains null bytes) or determined to be unsafe
* (e.g., attempts to traverse outside `rootDir` or current working directory if no `rootDir`).
*/
public sanitizePath(
input: string,
options: PathSanitizeOptions = {},
contextForLogging?: RequestContext,
): SanitizedPathInfo {
const opContext =
contextForLogging ||
requestContextService.createRequestContext({ operation: "sanitizePath" });
const originalInput = input;
const effectiveOptions: PathSanitizeOptions = {
toPosix: options.toPosix ?? false,
allowAbsolute: options.allowAbsolute ?? false,
rootDir: options.rootDir ? path.resolve(options.rootDir) : undefined, // Resolve rootDir upfront
};
let wasAbsoluteInitially = false;
let convertedToRelative = false;
try {
if (!input || typeof input !== "string") {
throw new Error("Invalid path input: must be a non-empty string.");
}
if (input.includes("\0")) {
throw new Error("Path contains null byte, which is disallowed.");
}
let normalized = path.normalize(input); // Normalize first (e.g., 'a/b/../c' -> 'a/c')
wasAbsoluteInitially = path.isAbsolute(normalized);
if (effectiveOptions.toPosix) {
normalized = normalized.replace(/\\/g, "/");
}
let finalSanitizedPath: string;
if (effectiveOptions.rootDir) {
// Resolve the input path against the root directory.
// If 'normalized' is absolute, path.resolve treats it as the new root.
// To correctly join, ensure 'normalized' is treated as relative to 'rootDir' if it's not already escaping.
let tempPathForResolve = normalized;
if (path.isAbsolute(normalized) && !effectiveOptions.allowAbsolute) {
// If absolute paths are not allowed, make it relative before resolving with rootDir
tempPathForResolve = normalized.replace(/^(?:[A-Za-z]:)?[/\\]+/, "");
convertedToRelative = true;
} else if (
path.isAbsolute(normalized) &&
effectiveOptions.allowAbsolute
) {
// Absolute path is allowed, check if it's within rootDir
if (
!normalized.startsWith(effectiveOptions.rootDir + path.sep) &&
normalized !== effectiveOptions.rootDir
) {
throw new Error(
"Absolute path is outside the specified root directory.",
);
}
finalSanitizedPath = path.relative(
effectiveOptions.rootDir,
normalized,
);
finalSanitizedPath =
finalSanitizedPath === "" ? "." : finalSanitizedPath; // Handle case where path is rootDir itself
// Early return if absolute path is allowed and within root.
return {
sanitizedPath: finalSanitizedPath,
originalInput,
wasAbsolute: wasAbsoluteInitially,
convertedToRelative,
optionsUsed: effectiveOptions,
};
}
// If path was relative or made relative, join with rootDir
const fullPath = path.resolve(
effectiveOptions.rootDir,
tempPathForResolve,
);
if (
!fullPath.startsWith(effectiveOptions.rootDir + path.sep) &&
fullPath !== effectiveOptions.rootDir
) {
throw new Error(
"Path traversal detected: sanitized path escapes root directory.",
);
}
finalSanitizedPath = path.relative(effectiveOptions.rootDir, fullPath);
finalSanitizedPath =
finalSanitizedPath === "" ? "." : finalSanitizedPath;
} else {
// No rootDir specified
if (path.isAbsolute(normalized)) {
if (effectiveOptions.allowAbsolute) {
finalSanitizedPath = normalized; // Absolute path allowed
} else {
// Convert to relative (strip leading slash/drive)
finalSanitizedPath = normalized.replace(
/^(?:[A-Za-z]:)?[/\\]+/,
"",
);
convertedToRelative = true;
}
} else {
// Path is relative, and no rootDir
// For relative paths without a rootDir, ensure they don't traverse "above" the conceptual CWD.
// path.resolve('.') gives current working directory.
const resolvedAgainstCwd = path.resolve(normalized);
if (!resolvedAgainstCwd.startsWith(path.resolve("."))) {
// This check is a bit tricky because '..' is valid if it stays within CWD's subtree.
// A more robust check might involve comparing segments or ensuring it doesn't go "too high".
// For simplicity, if it resolves outside CWD's prefix, consider it traversal.
// This might be too strict for some use cases but safer for general utility.
// A common pattern is to check if path.relative(cwd, resolvedPath) starts with '..'.
if (
path
.relative(path.resolve("."), resolvedAgainstCwd)
.startsWith("..")
) {
throw new Error(
"Relative path traversal detected (escapes current working directory context).",
);
}
}
finalSanitizedPath = normalized;
}
}
return {
sanitizedPath: finalSanitizedPath,
originalInput,
wasAbsolute: wasAbsoluteInitially,
convertedToRelative,
optionsUsed: effectiveOptions,
};
} catch (error) {
const message =
error instanceof Error ? error.message : "Invalid or unsafe path.";
logger.warning(`Path sanitization error: ${message}`, {
...opContext,
input: originalInput,
options: effectiveOptions,
errorDetails: String(error),
});
throw new McpError(BaseErrorCode.VALIDATION_ERROR, message, {
...opContext,
input: originalInput,
});
}
}
/**
* Sanitizes a JSON string by parsing it to validate its format.
* Optionally checks if the JSON string's byte size exceeds a maximum limit.
*
* @template T The expected type of the parsed JSON object. Defaults to `unknown`.
* @param {string} input - The JSON string to sanitize/validate.
* @param {number} [maxSizeBytes] - Optional maximum allowed size of the JSON string in bytes.
* @param {RequestContext} [contextForLogging] - Optional context for logging errors.
* @returns {T} The parsed JavaScript object.
* @throws {McpError} If the input is not a string, is not valid JSON, or exceeds `maxSizeBytes`.
*/
public sanitizeJson<T = unknown>(
input: string,
maxSizeBytes?: number,
contextForLogging?: RequestContext,
): T {
const opContext =
contextForLogging ||
requestContextService.createRequestContext({ operation: "sanitizeJson" });
try {
if (typeof input !== "string") {
throw new Error("Invalid input: expected a JSON string.");
}
if (
maxSizeBytes !== undefined &&
Buffer.byteLength(input, "utf8") > maxSizeBytes
) {
throw new McpError( // Throw McpError directly
BaseErrorCode.VALIDATION_ERROR,
`JSON content exceeds maximum allowed size of ${maxSizeBytes} bytes. Actual size: ${Buffer.byteLength(input, "utf8")} bytes.`,
{
...opContext,
size: Buffer.byteLength(input, "utf8"),
maxSize: maxSizeBytes,
},
);
}
const parsed = JSON.parse(input);
// Note: This function only validates JSON structure. It does not sanitize content within the JSON.
// For deep sanitization of object values, additional logic would be needed.
return parsed as T;
} catch (error) {
if (error instanceof McpError) throw error; // Re-throw if already McpError (e.g., size limit)
const message =
error instanceof Error ? error.message : "Invalid JSON format.";
logger.warning(`JSON sanitization failed: ${message}`, {
...opContext,
inputPreview: input.substring(0, 100),
errorDetails: String(error),
});
throw new McpError(BaseErrorCode.VALIDATION_ERROR, message, {
...opContext,
inputPreview:
input.length > 100 ? `${input.substring(0, 100)}...` : input,
});
}
}
/**
* Sanitizes a numeric input (number or string) by converting it to a number
* and optionally clamping it within a specified min/max range.
*
* @param {number | string} input - The numeric value or string representation of a number.
* @param {number} [min] - Optional minimum allowed value (inclusive).
* @param {number} [max] - Optional maximum allowed value (inclusive).
* @param {RequestContext} [contextForLogging] - Optional context for logging clamping or errors.
* @returns {number} The sanitized (and potentially clamped) number.
* @throws {McpError} If the input cannot be parsed into a valid, finite number.
*/
public sanitizeNumber(
input: number | string,
min?: number,
max?: number,
contextForLogging?: RequestContext,
): number {
const opContext =
contextForLogging ||
requestContextService.createRequestContext({
operation: "sanitizeNumber",
});
let value: number;
if (typeof input === "string") {
const trimmedInput = input.trim();
// Validator's isNumeric allows empty strings, so check explicitly.
if (trimmedInput === "" || !validator.isNumeric(trimmedInput)) {
throw new McpError(
BaseErrorCode.VALIDATION_ERROR,
"Invalid number format: string is not numeric or is empty.",
{ ...opContext, input },
);
}
value = parseFloat(trimmedInput);
} else if (typeof input === "number") {
value = input;
} else {
throw new McpError(
BaseErrorCode.VALIDATION_ERROR,
"Invalid input type: expected number or string.",
{ ...opContext, input: String(input) },
);
}
if (isNaN(value) || !isFinite(value)) {
throw new McpError(
BaseErrorCode.VALIDATION_ERROR,
"Invalid number value (NaN or Infinity).",
{ ...opContext, input },
);
}
let clamped = false;
let originalValueForLog = value; // Store original before clamping for logging
if (min !== undefined && value < min) {
value = min;
clamped = true;
}
if (max !== undefined && value > max) {
value = max;
clamped = true;
}
if (clamped) {
logger.debug("Number clamped to range.", {
...opContext,
originalValue: originalValueForLog,
min,
max,
finalValue: value,
});
}
return value;
}
/**
* Sanitizes an object or array for logging by deep cloning it and redacting fields
* whose names (case-insensitively) match any of the configured sensitive field names.
* Redacted fields are replaced with the string `'[REDACTED]'`.
*
* @param {unknown} input - The object, array, or other value to sanitize for logging.
* If input is not an object or array, it's returned as is.
* @param {RequestContext} [contextForLogging] - Optional context for logging errors during sanitization.
* @returns {unknown} A sanitized copy of the input, safe for logging.
* Returns `'[Log Sanitization Failed]'` if an unexpected error occurs during sanitization.
*/
public sanitizeForLogging(
input: unknown,
contextForLogging?: RequestContext,
): unknown {
const opContext =
contextForLogging ||
requestContextService.createRequestContext({
operation: "sanitizeForLogging",
});
try {
// Primitives and null are returned as is.
if (input === null || typeof input !== "object") {
return input;
}
// Use structuredClone if available (Node.js >= 17), otherwise fallback to JSON parse/stringify.
// JSON.parse(JSON.stringify(obj)) is a common way to deep clone, but has limitations
// (e.g., loses functions, undefined, Date objects become strings).
// For logging, this is often acceptable.
const clonedInput =
typeof structuredClone === "function"
? structuredClone(input)
: JSON.parse(JSON.stringify(input));
this.redactSensitiveFields(clonedInput);
return clonedInput;
} catch (error) {
logger.error(
"Error during log sanitization process.",
error instanceof Error ? error : undefined,
{
...opContext,
errorDetails: error instanceof Error ? error.message : String(error),
},
);
return "[Log Sanitization Failed]"; // Fallback string indicating sanitization failure
}
}
/**
* Helper to convert attribute format for sanitize-html.
* `sanitize-html` expects `allowedAttributes` in a specific format.
* This method assumes the input `attrs` (from `SanitizeStringOptions`)
* is already in the correct format or a compatible one.
* @param {Record<string, string[]>} attrs - Attributes configuration.
* @returns {sanitizeHtml.IOptions['allowedAttributes']} Attributes in `sanitize-html` format.
* @private
*/
private convertAttributesFormat(
attrs: Record<string, string[]>,
): sanitizeHtml.IOptions["allowedAttributes"] {
// The type Record<string, string[]> is compatible with sanitizeHtml.IOptions['allowedAttributes']
// which can be Record<string, Array<string | RegExp>> or boolean.
// No complex conversion needed if options.allowedAttributes is already Record<string, string[]>.
return attrs;
}
/**
* Recursively redacts sensitive fields within an object or array.
* This method modifies the input object/array in place.
* @param {unknown} obj - The object or array to redact sensitive fields from.
* @private
*/
private redactSensitiveFields(obj: unknown): void {
if (!obj || typeof obj !== "object") {
return; // Not an object or array, or null
}
if (Array.isArray(obj)) {
obj.forEach((item) => {
// Recurse only if the item is an object (including nested arrays)
if (item && typeof item === "object") {
this.redactSensitiveFields(item);
}
});
return;
}
// It's an object (but not an array)
for (const key in obj) {
// Check if the property belongs to the object itself, not its prototype
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const value = (obj as Record<string, unknown>)[key];
const lowerKey = key.toLowerCase();
// Special handling for non-serializable but non-sensitive objects
if (key === "httpsAgent") {
(obj as Record<string, unknown>)[key] = "[HttpAgent Instance]";
continue; // Skip further processing for this key
}
// Check if the lowercase key includes any of the lowercase sensitive field terms
const isSensitive = this.sensitiveFields.some(
(field) => lowerKey.includes(field), // sensitiveFields are already stored as lowercase
);
if (isSensitive) {
(obj as Record<string, unknown>)[key] = "[REDACTED]";
} else if (value && typeof value === "object") {
// If the value is another object or array, recurse
this.redactSensitiveFields(value);
}
}
}
}
}
/**
* A default, shared instance of the `Sanitization` class.
* Use this instance for all sanitization tasks.
*
* Example:
* ```typescript
* import { sanitization, sanitizeInputForLogging } from './sanitization';
*
* const unsafeHtml = "<script>alert('xss')</script><p>Safe</p>";
* const safeHtml = sanitization.sanitizeHtml(unsafeHtml);
*
* const sensitiveData = { password: '123', username: 'user' };
* const safeLogData = sanitizeInputForLogging(sensitiveData);
* // safeLogData will be { password: '[REDACTED]', username: 'user' }
* ```
*/
export const sanitization = Sanitization.getInstance();
/**
* A convenience function that wraps `sanitization.sanitizeForLogging`.
* Sanitizes an object or array for logging by redacting sensitive fields.
*
* @param {unknown} input - The data to sanitize for logging.
* @param {RequestContext} [contextForLogging] - Optional context for logging errors during sanitization.
* @returns {unknown} A sanitized copy of the input, safe for logging.
*/
export const sanitizeInputForLogging = (
input: unknown,
contextForLogging?: RequestContext,
): unknown => sanitization.sanitizeForLogging(input, contextForLogging);