/** * Response Sanitization Middleware (inst_013, inst_045) * Prevents information disclosure in error responses * * QUICK WIN: Hide stack traces and sensitive data in production * NEVER expose: stack traces, internal paths, environment details */ /** * Sanitize error responses * Production: Generic error messages only * Development: More detailed errors for debugging */ function sanitizeErrorResponse(err, req, res, _next) { const isProduction = process.env.NODE_ENV === 'production'; // Log full error details internally (always) console.error('[ERROR]', { message: err.message, stack: err.stack, path: req.path, method: req.method, ip: req.ip, user: req.user?.id || req.user?.userId, timestamp: new Date().toISOString() }); // Determine status code const statusCode = err.statusCode || err.status || 500; // Generic error messages for common status codes const genericErrors = { 400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 413: 'Payload Too Large', 429: 'Too Many Requests', 500: 'Internal Server Error', 502: 'Bad Gateway', 503: 'Service Unavailable' }; // Production: Generic error messages only if (isProduction) { return res.status(statusCode).json({ error: genericErrors[statusCode] || 'Error', message: err.message || 'An error occurred' // NEVER include in production: stack, file paths, internal details }); } // Development: More detailed errors (but still sanitized) res.status(statusCode).json({ error: err.name || 'Error', message: err.message, statusCode, // Stack trace only in development stack: process.env.NODE_ENV === 'development' ? err.stack : undefined }); } /** * Remove sensitive fields from objects * Useful for sanitizing database results before sending to client */ function removeSensitiveFields(data, sensitiveFields = ['password', 'passwordHash', 'apiKey', 'secret', 'token'], visited = new WeakSet()) { if (Array.isArray(data)) { return data.map(item => removeSensitiveFields(item, sensitiveFields, visited)); } // Preserve Date objects (spreading them creates empty {}) if (data instanceof Date) { return data; } if (typeof data === 'object' && data !== null) { // Handle circular references if (visited.has(data)) { return '[Circular]'; } visited.add(data); // Convert Mongoose documents to plain objects const plainData = data.toObject ? data.toObject() : data; const sanitized = { ...plainData }; // Remove sensitive fields for (const field of sensitiveFields) { delete sanitized[field]; } // Recursively sanitize nested objects (but skip Dates) for (const key in sanitized) { if (typeof sanitized[key] === 'object' && sanitized[key] !== null) { sanitized[key] = removeSensitiveFields(sanitized[key], sensitiveFields, visited); } } return sanitized; } return data; } /** * Middleware to sanitize response data * Apply before sending responses with user/database data */ function sanitizeResponseData(req, res, next) { // Store original json method const originalJson = res.json.bind(res); // Override json method to sanitize data res.json = function(data) { const sanitized = removeSensitiveFields(data); return originalJson(sanitized); }; next(); } module.exports = { sanitizeErrorResponse, removeSensitiveFields, sanitizeResponseData };