CWE-209: Error Message Information Leak - JavaScript
Overview
Error Message Information Leak in JavaScript applications occurs when error stack traces, database query details, or internal system information is exposed through API responses, error pages, or client-side console output. Node.js applications with Express, Fastify, Koa, and Next.js each have different error handling mechanisms that must be properly configured to avoid information disclosure.
Primary Defence: Return generic error messages to clients while logging detailed errors server-side, use centralized error handling middleware, and check NODE_ENV to prevent stack traces in production.
Common Vulnerable Patterns
Returning Raw Error Messages in Express
// VULNERABLE - Exposes database errors and stack traces
const express = require('express');
const app = express();
app.get('/user/:id', async (req, res) => {
try {
const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
res.json(user);
} catch (error) {
// Returns: "table 'users' does not exist" or connection errors
res.status(500).json({ error: error.message });
}
});
Why this is vulnerable:
- Reveals table/column names, SQL syntax, and schema structure.
- Exposes database vendor details and error codes.
- Leaks connection details and internal topology.
- Enables targeted SQL/NoSQL injection planning.
Exposing Full Stack Traces
// VULNERABLE - Returns complete stack trace
app.post('/process', async (req, res) => {
try {
const result = await complexOperation(req.body);
res.json(result);
} catch (error) {
// Exposes: file paths, function names, line numbers, dependencies
res.status(500).json({
error: error.message,
stack: error.stack,
name: error.name
});
}
});
Why this is vulnerable:
- Exposes absolute paths, function names, and line numbers.
- Reveals module structure and third-party dependencies.
- Helps attackers fingerprint versions with known CVEs.
- Maps internal code paths for exploitation.
Default Express Error Handler
// VULNERABLE - Express default error handler shows stack traces
const express = require('express');
const app = express();
app.get('/data', async (req, res) => {
// If error thrown here, Express default handler exposes details
const data = await fetchData();
res.json(data);
});
// No custom error handler - uses Express default which shows:
// - Full stack trace in development
// - Error message in production
// - Internal paths and structure
Why this is vulnerable:
- Default handler leaks stack traces or error messages.
- Reveals middleware chain and routing structure.
- Exposes framework internals and file layout.
- Unhandled exceptions leak details by default.
Unhandled Promise Rejections
// VULNERABLE - Unhandled rejections may log sensitive data
app.get('/async-data', (req, res) => {
// Promise rejection not caught
fetchDataFromAPI(req.params.id)
.then(data => res.json(data));
// If fetchDataFromAPI rejects, error details may be logged or exposed
});
// Unhandled rejection logging
process.on('unhandledRejection', (reason, promise) => {
// May log sensitive data to console or logs
console.error('Unhandled Rejection:', reason);
});
Why this is vulnerable:
- Unhandled rejections log full error details to stderr.
- Error text can include secrets or connection strings.
- Logs may be accessible via container/monitoring systems.
- Attackers can trigger errors to probe behavior.
Next.js Development Mode Errors
// VULNERABLE - Next.js dev mode shows detailed error overlay
// next.config.js
module.exports = {
// No production optimization
reactStrictMode: true,
}
// pages/api/users/[id].js
export default async function handler(req, res) {
try {
const user = await prisma.user.findUnique({
where: { id: req.query.id }
});
res.json(user);
} catch (error) {
// In dev mode, Next.js shows full error overlay with stack trace
throw error;
}
}
Why this is vulnerable:
- Dev overlays show stack traces and source snippets.
- Exposes file paths, schema details, and configs.
- Misconfigured prod deploys leak full internals.
- Attackers can trigger errors to view details.
Logging Sensitive Data to Console
// VULNERABLE - Console logs may be visible
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Logs plaintext password!
console.log('Login attempt:', { username, password });
try {
const token = await authenticateUser(username, password);
// Logs sensitive token!
console.log('Generated token:', token);
res.json({ token });
} catch (error) {
console.error('Auth error:', error);
res.status(401).json({ error: 'Authentication failed' });
}
});
Why this is vulnerable:
- Logs may capture passwords, tokens, and session IDs.
- Aggregation systems make logs widely accessible.
- Plaintext credentials enable account takeover.
- Secrets persist long after the request.
Secure Patterns
Express Global Error Handler
// SECURE - Generic errors to users, detailed logs server-side
const express = require('express');
const winston = require('winston');
const { v4: uuidv4 } = require('uuid');
const app = express();
// Configure Winston logger
const logger = winston.createLogger({
level: 'error',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: '/var/log/app/error.log' })
]
});
// Route handlers
app.get('/user/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'Resource not found' });
}
res.json(user);
} catch (error) {
next(error); // Pass to error handler
}
});
// Global error handler (must be last middleware)
app.use((err, req, res, next) => {
const errorId = uuidv4();
// Log full details server-side
logger.error({
errorId,
message: err.message,
stack: err.stack,
path: req.path,
method: req.method,
timestamp: new Date().toISOString()
});
// Return generic message to client
res.status(500).json({
error: 'An error occurred',
errorId,
message: 'Please contact support with this error ID'
});
});
module.exports = app;
Why this works:
- Error handler runs last and catches upstream failures.
- Full details are logged server-side only.
- Clients receive generic messages with error IDs.
- Error IDs correlate support tickets to logs.
- Logs stay outside the web root with OS permissions.
Environment-Aware Error Handling
// SECURE - Different error handling for dev vs production
const isDevelopment = process.env.NODE_ENV !== 'production';
app.use((err, req, res, next) => {
const errorId = uuidv4();
// Always log server-side
logger.error({
errorId,
error: err.message,
stack: err.stack,
path: req.path
});
// Conditional response based on environment
const response = {
error: 'An error occurred',
errorId
};
// Only include details in development
if (isDevelopment) {
response.message = err.message;
response.stack = err.stack;
}
res.status(err.status || 500).json(response);
});
Why this works:
- Production responses omit stack traces and internals.
- Development keeps details for debugging.
- Full errors are always logged server-side.
- Error IDs provide consistent traceability.
- Requires
NODE_ENV=productionin deploys.
Async Error Wrapper
// SECURE - Wrapper to handle async errors consistently
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// Usage
app.get('/user/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
throw new NotFoundError('User not found');
}
res.json(user);
}));
// Custom error classes
class AppError extends Error {
constructor(message, statusCode = 500, isOperational = true) {
super(message);
this.statusCode = statusCode;
this.isOperational = isOperational;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(message = 'Resource not found') {
super(message, 404);
}
}
class ValidationError extends AppError {
constructor(message = 'Invalid input') {
super(message, 400);
}
}
// Error handler with custom errors
app.use((err, req, res, next) => {
const errorId = uuidv4();
// Log all errors
logger.error({
errorId,
message: err.message,
stack: err.stack,
isOperational: err.isOperational
});
// Return appropriate response
if (err.isOperational) {
// Known operational error - safe to send message
res.status(err.statusCode).json({
error: err.message,
errorId
});
} else {
// Unknown error - return generic message
res.status(500).json({
error: 'An unexpected error occurred',
errorId
});
}
});
Why this works:
- Async errors are forwarded to centralized middleware.
- Prevents unhandled rejection leakage.
- Error classes separate safe vs. unsafe messages.
isOperationalgates what is safe to expose.- Full details logged with generic client responses.
Fastify Error Handler
// SECURE - Fastify with custom error handling
const fastify = require('fastify')({ logger: true });
const { v4: uuidv4 } = require('uuid');
// Custom error handler
fastify.setErrorHandler((error, request, reply) => {
const errorId = uuidv4();
// Log full error details
fastify.log.error({
errorId,
error: error.message,
stack: error.stack,
path: request.url,
method: request.method
});
// Determine status code
const statusCode = error.statusCode || 500;
// Return generic error
reply.status(statusCode).send({
error: statusCode === 404 ? 'Resource not found' : 'An error occurred',
errorId
});
});
// Route with error
fastify.get('/user/:id', async (request, reply) => {
const user = await User.findById(request.params.id);
if (!user) {
return reply.code(404).send({ error: 'User not found' });
}
return user;
});
Why this works:
- Global handler blocks Fastify default detail exposure.
- Structured logging keeps full details server-side.
- Error IDs correlate client reports to logs.
- Status codes are preserved without leaking internals.
- Responses are serialized safely by Fastify.
Next.js API Error Handling
// SECURE - Next.js API routes with secure error handling
// pages/api/users/[id].js
import { v4 as uuidv4 } from 'uuid';
import logger from '../../../lib/logger';
export default async function handler(req, res) {
const errorId = uuidv4();
try {
const user = await prisma.user.findUnique({
where: { id: req.query.id }
});
if (!user) {
return res.status(404).json({ error: 'Resource not found' });
}
res.status(200).json(user);
} catch (error) {
// Log full error server-side
logger.error({
errorId,
message: error.message,
stack: error.stack,
path: req.url
});
// Return generic error
res.status(500).json({
error: 'An error occurred',
errorId
});
}
}
// next.config.js - Production configuration
module.exports = {
reactStrictMode: true,
productionBrowserSourceMaps: false, // Disable source maps in production
compiler: {
removeConsole: process.env.NODE_ENV === 'production', // Remove console logs
}
}
Why this works:
- API errors are caught and sanitized before response.
- Full details are logged with an error ID.
- Source maps are disabled in production.
- Console output is removed from production builds.
- 404s are handled explicitly to avoid DB errors.
Structured Logging with Redaction
// SECURE - Winston logger with sensitive data redaction
const winston = require('winston');
// Custom format to redact sensitive data
const redactSensitiveData = winston.format((info) => {
const sensitivePatterns = [
{ pattern: /password["']?\s*[:=]\s*["']?([^"'}\s]+)/gi, replacement: 'password=***REDACTED***' },
{ pattern: /token["']?\s*[:=]\s*["']?([^"'}\s]+)/gi, replacement: 'token=***REDACTED***' },
{ pattern: /\b\d{13,19}\b/g, replacement: '***CARD***' }, // Credit card numbers
{ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, replacement: '***EMAIL***' },
];
let message = typeof info.message === 'string' ? info.message : JSON.stringify(info.message);
sensitivePatterns.forEach(({ pattern, replacement }) => {
message = message.replace(pattern, replacement);
});
info.message = message;
return info;
});
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
redactSensitiveData(),
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({
filename: '/var/log/app/error.log',
level: 'error'
}),
new winston.transports.File({
filename: '/var/log/app/combined.log'
})
]
});
// Don't log to console in production
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
module.exports = logger;
Why this works:
- Redaction runs before writing to log files.
- Regex patterns catch common secret formats.
- Redaction protects even if devs log sensitive data.
- Console logging is disabled in production.
- Separate files support log rotation and access control.
Validation Error Sanitization
// SECURE - Sanitized validation errors
const { validationResult } = require('express-validator');
app.post('/register', [
body('email').isEmail(),
body('password').isLength({ min: 8 })
], (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
// Log detailed errors server-side
logger.warn('Validation failed', { errors: errors.array() });
// Return generic error to client
return res.status(400).json({
error: 'Invalid input provided'
});
}
// Process valid request
res.json({ status: 'success' });
});
Why this works:
- Detailed validation errors stay server-side.
- Clients receive generic messages only.
- Prevents leakage of field names and rules.
- 400 status is preserved without extra detail.
- Logs retain full context for debugging.
Verification
After implementing the recommended secure patterns, verify the fix through multiple approaches:
- Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
- Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
- Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
- Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
- Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
- Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
- Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
- Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced