Skip to main content
Glama
error-sanitizer.tsβ€’16.1 kB
/** * Error message sanitization utility to prevent information disclosure * * This module provides secure error handling by sanitizing error messages * that might expose sensitive information to potential attackers. */ import { error as logError, OperationType } from './logger.js'; /** * Types of sensitive information to remove from error messages */ enum SensitiveInfoType { FILE_PATH = 'file_path', API_KEY = 'api_key', INTERNAL_ID = 'internal_id', STACK_TRACE = 'stack_trace', DATABASE_SCHEMA = 'database_schema', SYSTEM_INFO = 'system_info', URL_WITH_PARAMS = 'url_with_params', EMAIL_ADDRESS = 'email_address', IP_ADDRESS = 'ip_address', HTML_TAGS = 'html_tags', JAVASCRIPT_CODE = 'javascript_code', NETWORK_INFO = 'network_info', } /** * Patterns for detecting sensitive information in error messages */ const SENSITIVE_PATTERNS: Record<SensitiveInfoType, RegExp> = { [SensitiveInfoType.FILE_PATH]: /(file:\/\/|([A-Z]:)?[/\\](?:Users|home|var|opt|etc|tmp|src|app)[/\\])[^\s"']+/gi, [SensitiveInfoType.API_KEY]: /(?:(?:api[\s_-]*key)|token|bearer|authorization|secret|password|passwd|pwd)[\s:=]*["']?[a-zA-Z0-9\-_]{20,}["']?/gi, [SensitiveInfoType.INTERNAL_ID]: /(?:workspace_id|record_id|object_id|user_id|session_id)[\s:=]*["']?[a-f0-9-]{20,}["']?/gi, [SensitiveInfoType.STACK_TRACE]: /\s*at\s+[^\n]+/gi, [SensitiveInfoType.DATABASE_SCHEMA]: /(?:table|column|field|attribute|slug)[\s:]+["']?[a-z_][a-z0-9_]*["']?/gi, [SensitiveInfoType.SYSTEM_INFO]: /(?:node|npm|v\d+\.\d+\.\d+|darwin|linux|win32|x64|x86)/gi, [SensitiveInfoType.URL_WITH_PARAMS]: /https?:\/\/[^\s]+\?[^\s]+/gi, [SensitiveInfoType.EMAIL_ADDRESS]: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/gi, [SensitiveInfoType.IP_ADDRESS]: /\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b/gi, [SensitiveInfoType.HTML_TAGS]: /<[^>]*>/gi, [SensitiveInfoType.JAVASCRIPT_CODE]: /(javascript:|alert\(|console\.|eval\(|setTimeout\(|setInterval\()[^"'\s]*/gi, [SensitiveInfoType.NETWORK_INFO]: /\b(localhost|127\.0\.0\.1|0\.0\.0\.0|host=[^\s]+|port=\d+|user=[^\s]+)\b/gi, }; /** * Interface for error-like objects that have message, name, and stack properties */ interface ErrorLike { message?: unknown; name?: unknown; stack?: unknown; } /** * Interface for Error objects with additional sanitized properties */ interface SanitizedErrorObject extends Error { statusCode?: number; type?: string; safeMetadata?: Record<string, unknown>; } /** * User-friendly error messages mapped by error type */ const USER_FRIENDLY_MESSAGES: Record<string, string> = { // Authentication & Authorization authentication: 'Authentication failed. Please check your credentials.', authorization: 'You do not have permission to perform this action.', forbidden: 'Access denied. This resource requires additional permissions.', unauthorized: 'Authentication required. Please provide valid credentials.', // Resource errors not_found: 'The requested resource could not be found.', resource_not_found: 'The specified record does not exist or you do not have access to it.', invalid_id: 'The provided ID is invalid. Please check and try again.', // Validation errors validation: 'The provided data is invalid. Please check your input.', invalid_format: 'The data format is incorrect. Please review the expected format.', missing_required: 'Required information is missing. Please provide all required fields.', duplicate: 'A record with this information already exists.', // Rate limiting rate_limit: 'Too many requests. Please wait a moment before trying again.', quota_exceeded: 'Usage quota exceeded. Please try again later.', // System errors internal_error: 'An internal error occurred. Please try again later.', service_unavailable: 'The service is temporarily unavailable. Please try again later.', timeout: 'The request took too long to process. Please try again.', network_error: 'A network error occurred. Please check your connection.', // Field-specific errors invalid_field: 'One or more fields contain invalid values.', unknown_field: 'Unknown field provided. Please check the available fields.', field_type_mismatch: 'Field value type mismatch. Please check the expected type.', // Default fallback default: 'An error occurred while processing your request.', }; /** * Map specific error patterns to error types */ function classifyError(message: string): string { const lowerMessage = message.toLowerCase(); if ( lowerMessage.includes('authentication') || lowerMessage.includes('api key') || lowerMessage.includes('api_key') ) { return 'authentication'; } if ( lowerMessage.includes('authorization') || lowerMessage.includes('permission') ) { return 'authorization'; } if (lowerMessage.includes('forbidden')) { return 'forbidden'; } if (lowerMessage.includes('not found')) { return 'not_found'; } if (lowerMessage.includes('invalid') && lowerMessage.includes('id')) { return 'invalid_id'; } if ( lowerMessage.includes('validation') || lowerMessage.includes('invalid value') ) { return 'validation'; } if (lowerMessage.includes('format')) { return 'invalid_format'; } if (lowerMessage.includes('required')) { return 'missing_required'; } if ( lowerMessage.includes('duplicate') || lowerMessage.includes('already exists') ) { return 'duplicate'; } if (lowerMessage.includes('rate limit')) { return 'rate_limit'; } if (lowerMessage.includes('timeout')) { return 'timeout'; } if (lowerMessage.includes('network')) { return 'network_error'; } if ( lowerMessage.includes('cannot find attribute') || lowerMessage.includes('unknown field') ) { return 'unknown_field'; } if ( lowerMessage.includes('internal') || lowerMessage.includes('server error') ) { return 'internal_error'; } return 'default'; } /** * Extract helpful context from error without exposing sensitive data */ function extractSafeContext(message: string): string | undefined { // Extract field names (but not values or system paths) const fieldMatch = message.match( /(?:field|attribute)[s]?\s+(?:with\s+)?["']?([a-z_]+)["']?/i ); if (fieldMatch && fieldMatch[1] && !fieldMatch[1].includes('/')) { return `Field: ${fieldMatch[1]}`; } // Extract resource type const resourceMatch = message.match( /\b(company|companies|person|people|deal|deals|task|tasks|record|records)\b/i ); if (resourceMatch) { return `Resource: ${resourceMatch[1].toLowerCase()}`; } return undefined; } /** * Options for error sanitization */ export interface SanitizationOptions { /** Include safe context in the sanitized message */ includeContext?: boolean; /** Log the full error internally before sanitizing */ logOriginal?: boolean; /** Module name for logging */ module?: string; /** Operation name for logging */ operation?: string; /** Additional safe metadata to include */ safeMetadata?: Record<string, unknown>; } /** * Sanitize an error message to remove sensitive information * * @param error - The error to sanitize (Error object or string) * @param options - Sanitization options * @returns Sanitized error message safe for external exposure */ export function sanitizeErrorMessage( error: Error | string | Record<string, unknown>, options: SanitizationOptions = {} ): string { const { includeContext = true, logOriginal = true, module = 'error-sanitizer', operation = 'sanitize', safeMetadata = {}, } = options; // Extract the original message let originalMessage: string; let errorName = 'Error'; let stackTrace: string | undefined; if (error instanceof Error) { originalMessage = error.message; errorName = error.name; stackTrace = error.stack; } else if (typeof error === 'string') { originalMessage = error; } else if (error && typeof error === 'object' && 'message' in error) { const errorLike = error as ErrorLike; originalMessage = String(errorLike.message); errorName = String(errorLike.name || 'Error'); stackTrace = String(errorLike.stack || ''); } else { originalMessage = String(error); } // Log the original error internally if requested if (logOriginal && process.env.NODE_ENV !== 'production') { logError( module, `Original error (internal only): ${originalMessage}`, { name: errorName, stack: stackTrace, ...safeMetadata }, undefined, operation, OperationType.SYSTEM ); } // Remove sensitive patterns let sanitized = originalMessage; // Remove HTML tags first to prevent XSS (Issue #836) sanitized = sanitized.replace( SENSITIVE_PATTERNS[SensitiveInfoType.HTML_TAGS], '' ); // Remove JavaScript code (Issue #836) sanitized = sanitized.replace( SENSITIVE_PATTERNS[SensitiveInfoType.JAVASCRIPT_CODE], '[JS_REDACTED]' ); // Remove file paths sanitized = sanitized.replace( SENSITIVE_PATTERNS[SensitiveInfoType.FILE_PATH], '[PATH_REDACTED]' ); // Remove API keys and tokens sanitized = sanitized.replace( SENSITIVE_PATTERNS[SensitiveInfoType.API_KEY], '[CREDENTIAL_REDACTED]' ); // Remove internal IDs (but keep generic reference) sanitized = sanitized.replace( SENSITIVE_PATTERNS[SensitiveInfoType.INTERNAL_ID], '[ID_REDACTED]' ); // Remove stack traces sanitized = sanitized.replace( SENSITIVE_PATTERNS[SensitiveInfoType.STACK_TRACE], '' ); // Remove URLs with parameters sanitized = sanitized.replace( SENSITIVE_PATTERNS[SensitiveInfoType.URL_WITH_PARAMS], '[URL_REDACTED]' ); // Remove email addresses sanitized = sanitized.replace( SENSITIVE_PATTERNS[SensitiveInfoType.EMAIL_ADDRESS], '[EMAIL_REDACTED]' ); // Remove IP addresses sanitized = sanitized.replace( SENSITIVE_PATTERNS[SensitiveInfoType.IP_ADDRESS], '[IP_REDACTED]' ); // Remove network information (localhost, hosts, ports, users) sanitized = sanitized.replace( SENSITIVE_PATTERNS[SensitiveInfoType.NETWORK_INFO], '[NETWORK_REDACTED]' ); // Get user-friendly message based on error classification const errorType = classifyError(originalMessage); let userMessage = USER_FRIENDLY_MESSAGES[errorType] || USER_FRIENDLY_MESSAGES.default; // Add safe context if available and requested if (includeContext) { const safeContext = extractSafeContext(originalMessage); if (safeContext) { userMessage = `${userMessage} (${safeContext})`; } } // In production, return only the user-friendly message if (process.env.NODE_ENV === 'production') { return userMessage; } // In development, include sanitized technical details return `${userMessage}\n[Dev Info: ${sanitized.substring(0, 200)}${ sanitized.length > 200 ? '...' : '' }]`; } /** * Create a sanitized error object with safe properties */ export interface SanitizedError { message: string; type: string; statusCode?: number; safeMetadata?: Record<string, unknown>; } /** * Create a fully sanitized error object * * @param error - The error to sanitize * @param statusCode - Optional HTTP status code * @param options - Sanitization options * @returns Sanitized error object */ export function createSanitizedError( error: Error | string | Record<string, unknown>, statusCode?: number, options: SanitizationOptions = {} ): SanitizedError { const sanitizedMessage = sanitizeErrorMessage(error, options); // First try to classify by message content const messageBasedType = classifyError( error instanceof Error ? error.message : String(error) ); // If we have a status code and message classification returned 'default', // derive type from status code instead (more reliable) let errorType = messageBasedType; const finalStatusCode = statusCode ?? inferStatusCode(messageBasedType); if (statusCode && messageBasedType === 'default') { errorType = inferTypeFromStatusCode(statusCode); } // Map 'default' to 'sanitized_error' for consistency with secure-error-handler if (errorType === 'default') { errorType = 'sanitized_error'; } return { message: sanitizedMessage, type: errorType, statusCode: finalStatusCode, safeMetadata: options.safeMetadata, }; } /** * Infer HTTP status code from error type */ function inferStatusCode(errorType: string): number { switch (errorType) { case 'authentication': return 401; case 'authorization': case 'forbidden': return 403; case 'not_found': case 'resource_not_found': return 404; case 'validation': case 'invalid_format': case 'missing_required': case 'invalid_id': case 'unknown_field': case 'field_type_mismatch': return 400; case 'duplicate': return 409; case 'rate_limit': case 'quota_exceeded': return 429; case 'timeout': return 408; case 'service_unavailable': return 503; case 'internal_error': case 'network_error': default: return 500; } } /** * Infer error type from HTTP status code * Returns types consistent with secure-error-handler conventions (e.g., authentication_error) */ function inferTypeFromStatusCode(statusCode: number): string { if (statusCode === 400) return 'validation_error'; if (statusCode === 401) return 'authentication_error'; if (statusCode === 403) return 'authorization_error'; if (statusCode === 404) return 'not_found'; if (statusCode === 408) return 'timeout'; if (statusCode === 409) return 'duplicate'; if (statusCode === 429) return 'rate_limit'; if (statusCode === 503) return 'service_unavailable'; if (statusCode >= 500) return 'server_error'; return 'sanitized_error'; } /** * Type alias for async functions that can be wrapped with error sanitization */ type AsyncFunction = ( ...args: Record<string, unknown>[] ) => Promise<Record<string, unknown>>; /** * Middleware-style error sanitizer for wrapping async functions * * @param fn - The async function to wrap * @param options - Sanitization options * @returns Wrapped function that sanitizes errors */ export function withErrorSanitization<T extends AsyncFunction>( fn: T, options: SanitizationOptions = {} ): T { return (async (...args: Parameters<T>) => { try { return await fn(...args); } catch (error: unknown) { const sanitized = createSanitizedError( error as Error | string | Record<string, unknown>, undefined, options ); const sanitizedError = new Error( sanitized.message ) as SanitizedErrorObject; sanitizedError.name = 'SanitizedError'; sanitizedError.statusCode = sanitized.statusCode; sanitizedError.type = sanitized.type; sanitizedError.safeMetadata = sanitized.safeMetadata; throw sanitizedError; } }) as T; } /** * Check if a message contains sensitive information * * @param message - The message to check * @returns True if sensitive information is detected */ export function containsSensitiveInfo(message: string): boolean { for (const pattern of Object.values(SENSITIVE_PATTERNS)) { if (pattern.test(message)) { return true; } } return false; } /** * Get a safe error summary for logging or metrics * * @param error - The error to summarize * @returns Safe summary string */ export function getErrorSummary( error: Error | string | Record<string, unknown> ): string { const errorType = classifyError( error instanceof Error ? error.message : String(error) ); const safeContext = extractSafeContext( error instanceof Error ? error.message : String(error) ); return safeContext ? `${errorType} (${safeContext})` : errorType; } export default { sanitizeErrorMessage, createSanitizedError, withErrorSanitization, containsSensitiveInfo, getErrorSummary, };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kesslerio/attio-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server