/** * Sanitizes errors before recording them in OpenTelemetry spans to prevent * sensitive information from being sent to DataDog or other observability platforms. */ // TODO(alex): check if this is still needed once o11y-refactor is complete // Patterns that indicate sensitive information const SENSITIVE_PATTERNS = [ // Tokens and keys - capture groups to preserve prefixes /(\bbearer\s+)[a-zA-Z0-9\-._~+/]+=*/gi, /(\bbasic\s+)\S+/gi, // Basic auth can contain any non-whitespace characters /(\bjwt\s+)[a-zA-Z0-9\-._~+/]+=*/gi, /(\btoken[:\s=]+)[a-zA-Z0-9\-._~+/]{16,}=*/gi, /(\bapi[_\s]?key[:\s=]+)[a-zA-Z0-9\-._~+/]{16,}=*/gi, /(\baccess[_\s]?token[:\s=]+)[a-zA-Z0-9\-._~+/]{16,}=*/gi, /(\brefresh[_\s]?token[:\s=]+)[a-zA-Z0-9\-._~+/]{16,}=*/gi, // JWT pattern (base64.base64.base64) - standalone tokens /\b[A-Za-z0-9-_]{20,}\.[A-Za-z0-9-_]{20,}\.[A-Za-z0-9-_]{20,}\b/g ]; // Fields to completely remove from error objects const COMPLETELY_REDACTED_FIELDS = [ 'token', 'accesstoken', 'access_token', 'refreshtoken', 'refresh_token', 'jwt', 'bearer', 'apikey', 'api_key', 'secret', 'password', 'passwd', 'key', 'private_key', 'privatekey', 'credentials', 'auth', 'session', //TODO(alex): confirm whether we can remove this 'cookie' ]; // Fields that should have their values sanitized but structure preserved const VALUE_SANITIZED_FIELDS = ['authorization', 'headers']; /** * Sanitizes a message string by replacing sensitive patterns */ function sanitizeMessage(message: string): string { if (!message) return message; let sanitized = message; for (const pattern of SENSITIVE_PATTERNS) { if (pattern.global) { pattern.lastIndex = 0; // Reset regex state } if (pattern.source.includes('(')) { // Pattern has capture groups - preserve prefix sanitized = sanitized.replace(pattern, '$1[REDACTED]'); } else { // Pattern matches entire sensitive value sanitized = sanitized.replace(pattern, '[REDACTED]'); } } return sanitized; } /** * Sanitizes a stack trace by replacing sensitive patterns */ function sanitizeStackTrace(stack: string): string { if (!stack) return stack; return sanitizeMessage(stack); } /** * Recursively sanitizes an object by removing/redacting sensitive fields */ function sanitizeObject(obj: unknown, depth = 0): unknown { if (depth > 5) { // Prevent infinite recursion return '[MAX_DEPTH_REACHED]'; } if (obj === null || obj === undefined) { return obj; } if (typeof obj === 'string') { return sanitizeMessage(obj); } if (typeof obj === 'number' || typeof obj === 'boolean') { return obj; } if (typeof obj !== 'object') { return obj; } if (Array.isArray(obj)) { return obj.map((item) => sanitizeObject(item, depth + 1)); } const sanitized: Record = {}; for (const [key, value] of Object.entries(obj)) { const lowerKey = key.toLowerCase(); // Check for value-sanitized fields first (more specific) if (VALUE_SANITIZED_FIELDS.some((field) => lowerKey === field)) { if (typeof value === 'string') { sanitized[key] = sanitizeMessage(value); } else { sanitized[key] = sanitizeObject(value, depth + 1); } continue; } // Then check for completely redacted fields if (COMPLETELY_REDACTED_FIELDS.some((field) => lowerKey === field || lowerKey.includes(field))) { sanitized[key] = '[REDACTED]'; continue; } // Special handling for common error properties if (key === 'message') { sanitized[key] = sanitizeMessage(value as string); } else if (key === 'stack') { sanitized[key] = sanitizeStackTrace(value as string); } else { sanitized[key] = sanitizeObject(value, depth + 1); } } return sanitized; } // Public wrappers for general observability log sanitization export function sanitizeObservabilityMessage(message: string): string { return sanitizeMessage(message); } export function sanitizeObservabilityValue(value: T): T { return sanitizeObject(value) as T; } /** * Sanitizes an error object for safe recording in OpenTelemetry spans */ export function sanitizeError(error: unknown): Error { if (!error) { return error as Error; } // If it's already a sanitized error, return as-is const errorWithSanitized = error as Error & { _sanitized?: boolean }; if (errorWithSanitized._sanitized) { return errorWithSanitized; } let sanitized: Error & { _sanitized?: boolean; [key: string]: unknown }; if (error instanceof Error) { // Create a new error with sanitized properties sanitized = new Error(sanitizeMessage(error.message)) as Error & { _sanitized?: boolean; [key: string]: unknown }; sanitized.name = error.name; sanitized.stack = sanitizeStackTrace(error.stack || ''); // Copy and sanitize all enumerable properties Object.keys(error).forEach((prop) => { if (prop !== 'message' && prop !== 'name' && prop !== 'stack') { const lowerProp = prop.toLowerCase(); const errorWithProps = error as unknown as Record; // Check for value-sanitized fields first (more specific) if (VALUE_SANITIZED_FIELDS.some((field) => lowerProp === field)) { if (typeof errorWithProps[prop] === 'string') { sanitized[prop] = sanitizeMessage(errorWithProps[prop] as string); } else { sanitized[prop] = sanitizeObject(errorWithProps[prop]); } } else if (COMPLETELY_REDACTED_FIELDS.some((field) => lowerProp === field || lowerProp.includes(field))) { sanitized[prop] = '[REDACTED]'; } else { sanitized[prop] = sanitizeObject(errorWithProps[prop]); } } }); // Also check non-enumerable properties that might have been added Object.getOwnPropertyNames(error).forEach((prop) => { if (prop !== 'message' && prop !== 'name' && prop !== 'stack' && !Object.prototype.propertyIsEnumerable.call(error, prop)) { const lowerProp = prop.toLowerCase(); const errorWithProps = error as unknown as Record; // Check for value-sanitized fields first (more specific) if (VALUE_SANITIZED_FIELDS.some((field) => lowerProp === field)) { if (typeof errorWithProps[prop] === 'string') { sanitized[prop] = sanitizeMessage(errorWithProps[prop] as string); } else { sanitized[prop] = sanitizeObject(errorWithProps[prop]); } } else if (COMPLETELY_REDACTED_FIELDS.some((field) => lowerProp === field || lowerProp.includes(field))) { sanitized[prop] = '[REDACTED]'; } else { sanitized[prop] = sanitizeObject(errorWithProps[prop]); } } }); } else { // For non-Error objects, sanitize the entire object const sanitizedObj = sanitizeObject(error); sanitized = sanitizedObj as Error & { _sanitized?: boolean; [key: string]: unknown }; } // Mark as sanitized to prevent double-processing sanitized._sanitized = true; return sanitized; }