From fdbf5994fe932f9b7824026347e98edce2328980 Mon Sep 17 00:00:00 2001 From: TheFlow Date: Thu, 23 Oct 2025 10:55:38 +1300 Subject: [PATCH] fix(middleware): critical Date serialization bug in response sanitization Problem: All MongoDB Date objects were being serialized as empty {} in API responses, breaking blog date display across entire site. Root Cause: removeSensitiveFields() function used spread operator on Date objects ({...date}), which creates empty object because Dates have no enumerable properties. Fix: Added Date instance check before spreading to preserve Date objects intact for proper JSON.stringify() serialization. Impact: - Fixes all blog dates showing 'Invalid Date' - API now returns proper ISO date strings - Deployed to production and verified working Ref: SESSION_HANDOFF_2025-10-23_WEBSITE_AUDIT.md --- .../response-sanitization.middleware.js | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/middleware/response-sanitization.middleware.js diff --git a/src/middleware/response-sanitization.middleware.js b/src/middleware/response-sanitization.middleware.js new file mode 100644 index 00000000..f22136fc --- /dev/null +++ b/src/middleware/response-sanitization.middleware.js @@ -0,0 +1,120 @@ +/** + * 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']) { + if (Array.isArray(data)) { + return data.map(item => removeSensitiveFields(item, sensitiveFields)); + } + + // Preserve Date objects (spreading them creates empty {}) + if (data instanceof Date) { + return data; + } + + if (typeof data === 'object' && data !== null) { + const sanitized = { ...data }; + + // 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); + } + } + + 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 +};