340 lines
9.5 KiB
JavaScript
340 lines
9.5 KiB
JavaScript
/**
|
|
* 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
|
|
};
|