/** * Secure Error Handler Utility * Prevents CWE-209: Information Exposure Through Error Messages * * This utility sanitizes error messages before sending them to clients, * ensuring that internal system details, file paths, stack traces, and * other sensitive information are never exposed to end users. */ const logger = require('./logger'); /** * Error types with user-friendly messages */ const ERROR_TYPES = { // Authentication & Authorization AUTH_FAILED: 'Authentication failed', AUTH_REQUIRED: 'Authentication required', AUTH_INVALID_TOKEN: 'Invalid or expired authentication token', AUTH_INSUFFICIENT_PERMISSIONS: 'Insufficient permissions', // User Management USER_NOT_FOUND: 'User not found', USER_ALREADY_EXISTS: 'User already exists', USER_CREATION_FAILED: 'Failed to create user', USER_UPDATE_FAILED: 'Failed to update user', USER_DELETE_FAILED: 'Failed to delete user', // Data Validation VALIDATION_FAILED: 'Validation failed', INVALID_INPUT: 'Invalid input provided', INVALID_FILE_TYPE: 'Invalid file type', FILE_TOO_LARGE: 'File size exceeds limit', MISSING_REQUIRED_FIELD: 'Required field is missing', // Database Operations DATABASE_ERROR: 'Database operation failed', RECORD_NOT_FOUND: 'Record not found', DUPLICATE_ENTRY: 'Duplicate entry exists', // File Operations FILE_NOT_FOUND: 'File not found', FILE_UPLOAD_FAILED: 'File upload failed', FILE_DELETE_FAILED: 'Failed to delete file', FILE_READ_FAILED: 'Failed to read file', FILE_WRITE_FAILED: 'Failed to write file', // Network & External Services NETWORK_ERROR: 'Network request failed', EXTERNAL_SERVICE_ERROR: 'External service unavailable', TIMEOUT_ERROR: 'Request timeout', // Rate Limiting RATE_LIMIT_EXCEEDED: 'Too many requests. Please try again later', // Generic INTERNAL_ERROR: 'An internal error occurred', NOT_FOUND: 'Resource not found', FORBIDDEN: 'Access forbidden', BAD_REQUEST: 'Bad request', CONFLICT: 'Resource conflict', UNPROCESSABLE_ENTITY: 'Unable to process request', SERVICE_UNAVAILABLE: 'Service temporarily unavailable' }; /** * Sanitize error for client response * Removes sensitive information like stack traces, file paths, and internal details * * @param {Error|string} error - The error to sanitize * @param {string} defaultMessage - Default message if error cannot be parsed * @returns {Object} Sanitized error object with safe message */ function sanitizeError(error, defaultMessage = ERROR_TYPES.INTERNAL_ERROR) { // If error is a string, return it as is (assuming it's already safe) if (typeof error === 'string') { return { message: error, code: 'CUSTOM_ERROR' }; } // Extract error message const errorMessage = error?.message || defaultMessage; // Check for known error patterns and map to safe messages // Database errors if (errorMessage.includes('UNIQUE constraint') || errorMessage.includes('UNIQUE')) { return { message: ERROR_TYPES.DUPLICATE_ENTRY, code: 'DUPLICATE_ENTRY' }; } if (errorMessage.includes('FOREIGN KEY constraint')) { return { message: ERROR_TYPES.CONFLICT, code: 'FOREIGN_KEY_CONSTRAINT' }; } if (errorMessage.includes('NOT NULL constraint')) { return { message: ERROR_TYPES.MISSING_REQUIRED_FIELD, code: 'MISSING_FIELD' }; } // File system errors if (errorMessage.includes('ENOENT') || errorMessage.includes('no such file')) { return { message: ERROR_TYPES.FILE_NOT_FOUND, code: 'FILE_NOT_FOUND' }; } if (errorMessage.includes('EACCES') || errorMessage.includes('permission denied')) { return { message: ERROR_TYPES.FORBIDDEN, code: 'PERMISSION_DENIED' }; } if (errorMessage.includes('ENOSPC') || errorMessage.includes('no space')) { return { message: ERROR_TYPES.SERVICE_UNAVAILABLE, code: 'DISK_FULL' }; } // Network errors if (errorMessage.includes('ECONNREFUSED') || errorMessage.includes('connection refused')) { return { message: ERROR_TYPES.EXTERNAL_SERVICE_ERROR, code: 'CONNECTION_REFUSED' }; } if (errorMessage.includes('ETIMEDOUT') || errorMessage.includes('timeout')) { return { message: ERROR_TYPES.TIMEOUT_ERROR, code: 'TIMEOUT' }; } if (errorMessage.includes('ENOTFOUND') || errorMessage.includes('getaddrinfo')) { return { message: ERROR_TYPES.NETWORK_ERROR, code: 'DNS_ERROR' }; } // Authentication errors if (errorMessage.toLowerCase().includes('unauthorized') || errorMessage.toLowerCase().includes('authentication')) { return { message: ERROR_TYPES.AUTH_FAILED, code: 'AUTH_ERROR' }; } if (errorMessage.toLowerCase().includes('forbidden') || errorMessage.toLowerCase().includes('permission')) { return { message: ERROR_TYPES.AUTH_INSUFFICIENT_PERMISSIONS, code: 'PERMISSION_ERROR' }; } // Validation errors (pass through if they seem safe) if (errorMessage.toLowerCase().includes('validation') || errorMessage.toLowerCase().includes('invalid')) { // Check if message is reasonably safe (no paths, no system info) if (!containsSensitiveInfo(errorMessage)) { return { message: errorMessage, code: 'VALIDATION_ERROR' }; } return { message: ERROR_TYPES.VALIDATION_FAILED, code: 'VALIDATION_ERROR' }; } // Default to generic error message return { message: defaultMessage, code: 'INTERNAL_ERROR' }; } /** * Check if error message contains sensitive information * * @param {string} message - Error message to check * @returns {boolean} True if message contains sensitive info */ function containsSensitiveInfo(message) { const sensitivePatterns = [ /\/[a-z0-9_\-\/]+\.(js|json|db|log|conf|env)/i, // File paths /at\s+[a-zA-Z0-9_]+\s+\(/i, // Stack trace patterns /line\s+\d+/i, // Line numbers /column\s+\d+/i, // Column numbers /Error:\s+SQLITE_/i, // SQLite internal errors /node_modules/i, // Node modules paths /\/home\//i, // Unix home directory /\/usr\//i, // Unix system paths /\/var\//i, // Unix var paths /\/tmp\//i, // Temp directory /C:\\/i, // Windows paths /\\Users\\/i, // Windows user paths /password/i, // Password references /secret/i, // Secret references /token/i, // Token references /key/i // Key references (be careful with "keyboard" etc.) ]; return sensitivePatterns.some(pattern => pattern.test(message)); } /** * Log error securely (internal logs can contain full details) * * @param {Error|string} error - Error to log * @param {Object} context - Additional context */ function logError(error, context = {}) { const errorInfo = { message: error?.message || error, stack: error?.stack, code: error?.code, ...context }; logger.error('Application error:', errorInfo); } /** * Express error handler middleware * Catches all errors and returns sanitized responses * * @param {Error} err - Error object * @param {Object} req - Express request * @param {Object} res - Express response * @param {Function} next - Express next function */ function errorMiddleware(err, req, res, next) { // Log full error details internally logError(err, { method: req.method, path: req.path, userId: req.user?.id, ip: req.ip }); // Determine status code const statusCode = err.statusCode || err.status || 500; // Sanitize error for client const sanitized = sanitizeError(err, ERROR_TYPES.INTERNAL_ERROR); // Send sanitized response res.status(statusCode).json({ error: sanitized.message, code: sanitized.code, timestamp: new Date().toISOString() }); } /** * Async handler wrapper to catch async errors * * @param {Function} fn - Async route handler * @returns {Function} Wrapped function */ function asyncHandler(fn) { return (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; } /** * Create a safe error response object * * @param {string} message - Error message (should be user-safe) * @param {number} statusCode - HTTP status code * @param {string} code - Error code * @returns {Object} Error response object */ function createError(message, statusCode = 500, code = 'ERROR') { const error = new Error(message); error.statusCode = statusCode; error.code = code; return error; } /** * Standard error responses */ const ErrorResponses = { badRequest: (message = ERROR_TYPES.BAD_REQUEST) => createError(message, 400, 'BAD_REQUEST'), unauthorized: (message = ERROR_TYPES.AUTH_REQUIRED) => createError(message, 401, 'UNAUTHORIZED'), forbidden: (message = ERROR_TYPES.FORBIDDEN) => createError(message, 403, 'FORBIDDEN'), notFound: (message = ERROR_TYPES.NOT_FOUND) => createError(message, 404, 'NOT_FOUND'), conflict: (message = ERROR_TYPES.CONFLICT) => createError(message, 409, 'CONFLICT'), unprocessable: (message = ERROR_TYPES.UNPROCESSABLE_ENTITY) => createError(message, 422, 'UNPROCESSABLE_ENTITY'), tooManyRequests: (message = ERROR_TYPES.RATE_LIMIT_EXCEEDED) => createError(message, 429, 'TOO_MANY_REQUESTS'), internal: (message = ERROR_TYPES.INTERNAL_ERROR) => createError(message, 500, 'INTERNAL_ERROR'), serviceUnavailable: (message = ERROR_TYPES.SERVICE_UNAVAILABLE) => createError(message, 503, 'SERVICE_UNAVAILABLE') }; module.exports = { ERROR_TYPES, sanitizeError, containsSensitiveInfo, logError, errorMiddleware, asyncHandler, createError, ErrorResponses };