Error Exposure
What is Error Exposure?
Section titled “What is Error Exposure?”Error Exposure is revealing internal system details through error messages, giving attackers valuable information:
// Tells attacker about your stackres.json({ error: 'QueryFailedError: relation "users" does not exist', stack: 'at PostgresDriver.query (node_modules/...)', query: 'SELECT * FROM users WHERE id = 1',})Real Example from the Project
Section titled “Real Example from the Project”src/utils/error.handling.js
// ANTIPATTERN: Error Exposure!
// Exposing full stack tracesexport function exposeEverything(error, res) { return res.json({ error: error.message, stack: error.stack, code: error.code, errno: error.errno, syscall: error.syscall, })}
// Leaking SQL queriesexport function leakDatabaseDetails(error, res) { return res.json({ error: 'Database error', query: error.query, // Exposes table structure! parameters: error.parameters, // Exposes data! detail: error.detail, // PostgreSQL details })}
// Exposing file pathsexport function leakFilePaths(error, res) { return res.json({ error: error.message, path: error.path, // Shows server structure // e.g., /var/www/app/src/services/user.js:45:12 })}
// Exposing configurationexport function leakConfig(error, res) { return res.json({ error: 'Connection failed', host: process.env.DB_HOST, port: process.env.DB_PORT, database: process.env.DB_NAME, // Attacker now knows your database! })}Dangerous Debug Mode
Section titled “Dangerous Debug Mode”// Debug mode left on in production!app.onError((error, c) => { if (process.env.DEBUG || true) { // Always true! return c.json({ error: error.message, stack: error.stack, env: process.env, // ALL environment variables! request: { headers: Object.fromEntries(c.req.raw.headers), body: await c.req.text(), }, }, 500) }})What Attackers Learn
Section titled “What Attackers Learn”From Stack Traces
Section titled “From Stack Traces”Error: ENOENT: no such file or directory at /var/www/myapp/src/services/storage.js:42:15 at PostgresDriver.query (node_modules/typeorm/...)- Framework: TypeORM
- Node.js path structure
- File names and line numbers
- Potential attack vectors
From Database Errors
Section titled “From Database Errors”QueryFailedError: column "admin_password" does not exist- Table structure
- Column names
- Database type
From Validation Errors
Section titled “From Validation Errors”Error: Field "ssn" must match pattern ^\d{3}-\d{2}-\d{4}$- What fields exist
- Expected data formats
- Validation rules to bypass
Why It’s Bad
Section titled “Why It’s Bad”1. Information Disclosure (CWE-209)
Section titled “1. Information Disclosure (CWE-209)”OWASP Top 10 security vulnerability. Attackers use this to:
- Map your infrastructure
- Identify vulnerable versions
- Plan targeted attacks
2. SQL Injection Guidance
Section titled “2. SQL Injection Guidance”// Error: You have an error in your SQL syntax near 'OR 1=1--'// Attacker now knows: SQL injection works here!// They refine: ' OR 1=1--// They enumerate: ' UNION SELECT table_name FROM information_schema.tables--3. Path Traversal Hints
Section titled “3. Path Traversal Hints”// Error: Cannot read file '/etc/passwd'// Attacker learns: Path traversal might work// Attacker tries: ../../../etc/passwd4. Version Disclosure
Section titled “4. Version Disclosure”// Error at express@4.17.1/lib/router/layer.js// Attacker checks: CVE database for express 4.17.1// Attacker finds: Known vulnerability!The Right Way
Section titled “The Right Way”1. Separate Internal and External Errors
Section titled “1. Separate Internal and External Errors”export function errorHandler(err, c) { const requestId = c.get('requestId') || crypto.randomUUID()
// INTERNAL: Log everything for debugging logger.error('Request failed', { requestId, error: err.message, stack: err.stack, path: c.req.path, method: c.req.method, userId: c.get('user')?.id, // Full context for debugging })
// EXTERNAL: Safe response for users return c.json({ error: getPublicMessage(err), code: getErrorCode(err), requestId, // For support lookup }, getStatusCode(err))}
function getPublicMessage(err) { // Map internal errors to safe messages if (err.name === 'ValidationError') { return err.message // Safe, we control this } if (err.name === 'NotFoundError') { return 'Resource not found' } if (err.code === 'ECONNREFUSED') { return 'Service temporarily unavailable' } // Default: never expose internal details return 'An unexpected error occurred'}
function getStatusCode(err) { const statusMap = { ValidationError: 400, NotFoundError: 404, AuthenticationError: 401, AuthorizationError: 403, } return statusMap[err.name] || 500}2. Error Code System
Section titled “2. Error Code System”// Error codes that are safe to exposeexport const ErrorCodes = { VALIDATION_FAILED: 'VALIDATION_FAILED', NOT_FOUND: 'NOT_FOUND', UNAUTHORIZED: 'UNAUTHORIZED', FORBIDDEN: 'FORBIDDEN', RATE_LIMITED: 'RATE_LIMITED', SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE', INTERNAL_ERROR: 'INTERNAL_ERROR',}
// User-facing messagesexport const ErrorMessages = { VALIDATION_FAILED: 'The provided data is invalid', NOT_FOUND: 'The requested resource was not found', UNAUTHORIZED: 'Authentication is required', FORBIDDEN: 'You do not have permission', RATE_LIMITED: 'Too many requests, please slow down', SERVICE_UNAVAILABLE: 'Service temporarily unavailable', INTERNAL_ERROR: 'An unexpected error occurred',}3. Safe Validation Errors
Section titled “3. Safe Validation Errors”// Safe: tells user what's wrong without exposing internalsfunction validateUser(data) { const errors = []
if (!data.email) { errors.push({ field: 'email', message: 'Email is required' }) } else if (!isValidEmail(data.email)) { errors.push({ field: 'email', message: 'Invalid email format' }) }
if (!data.password) { errors.push({ field: 'password', message: 'Password is required' }) } else if (data.password.length < 12) { errors.push({ field: 'password', message: 'Password must be at least 12 characters' }) } // Don't expose: regex patterns, allowed characters list, etc.
return errors}4. Environment-Aware Responses
Section titled “4. Environment-Aware Responses”const isDevelopment = process.env.NODE_ENV === 'development'
export function errorHandler(err, c) { const response = { error: getPublicMessage(err), code: getErrorCode(err), requestId: c.get('requestId'), }
// ONLY in development, never in production if (isDevelopment) { response.debug = { message: err.message, stack: err.stack, } }
return c.json(response, getStatusCode(err))}5. Centralized Logging
Section titled “5. Centralized Logging”import pino from 'pino'
const logger = pino({ level: process.env.LOG_LEVEL || 'info', // Redact sensitive fields redact: { paths: [ 'password', 'secret', 'token', 'authorization', '*.password', '*.secret', ], censor: '[REDACTED]', },})
export default logger
// Usagelogger.error('Database error', { error: err.message, password: 'secret123', // Logged as [REDACTED] user: { id: 1, password: 'hash', // Also [REDACTED] },})Comparison
Section titled “Comparison”| Exposed Error | Safe Error |
|---|---|
| Stack traces | Request ID for lookup |
| SQL queries | ”Database error” |
| File paths | ”Resource not found” |
| Config values | ”Service unavailable” |
| Internal types | Error codes |
| Regex patterns | ”Invalid format” |
Response Examples
Section titled “Response Examples”Bad Response ❌
Section titled “Bad Response ❌”{ "error": "ER_PARSE_ERROR: You have an error in your SQL syntax near 'SELECT * FROM users WHERE id = 1; DROP TABLE users;--'", "stack": "at Query.Sequence._packetToError (/var/www/app/node_modules/mysql/lib/protocol/...)", "query": "SELECT * FROM users WHERE id = ?", "sql": "SELECT * FROM users WHERE id = '1; DROP TABLE users;--'"}Good Response ✅
Section titled “Good Response ✅”{ "error": "Unable to process request", "code": "INTERNAL_ERROR", "requestId": "req_1234567890"}Checklist
Section titled “Checklist”Never Expose
Section titled “Never Expose”- Stack traces
- SQL queries
- File paths
- Environment variables
- Internal IP addresses
- Software versions
- Database table/column names
- Validation regex patterns
Safe to Expose
Section titled “Safe to Expose”- Error codes
- User-friendly messages
- Request IDs
- Field names for validation errors
- Public documentation links