Initial commit: StreamFlow IPTV platform
This commit is contained in:
commit
73a8ae9ffd
1240 changed files with 278451 additions and 0 deletions
340
backend/utils/errorHandler.js
Normal file
340
backend/utils/errorHandler.js
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
/**
|
||||
* 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
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue