Error Handling and Information Leakage
AI-generated error handling is designed for debugging, not production. It returns full stack traces to the client, logs passwords in request bodies, exposes database table names in error messages, and leaves debug endpoints accessible. Every piece of information leaked in an error response is reconnaissance for an attacker. ## Key Points - Whether an email exists in the system (user enumeration) - Database table and column names - Full stack traces with file paths - Internal query structure
skilldb get vibe-coding-security-skills/error-handling-information-leakageFull skill: 391 linesError Handling and Information Leakage
AI-generated error handling is designed for debugging, not production. It returns full stack traces to the client, logs passwords in request bodies, exposes database table names in error messages, and leaves debug endpoints accessible. Every piece of information leaked in an error response is reconnaissance for an attacker.
This skill teaches you to handle errors safely: informative for developers in logs, opaque for users in responses.
The Information Leakage Pattern
What AI generates:
app.post('/api/login', async (req, res) => {
try {
const user = await db.query('SELECT * FROM users WHERE email = $1', [req.body.email]);
if (!user.rows.length) {
return res.status(404).json({ error: 'User not found with email: ' + req.body.email });
}
const valid = await bcrypt.compare(req.body.password, user.rows[0].password_hash);
if (!valid) {
return res.status(401).json({ error: 'Invalid password for user: ' + req.body.email });
}
res.json({ token: generateToken(user.rows[0]) });
} catch (err) {
res.status(500).json({
error: err.message,
stack: err.stack,
query: 'SELECT * FROM users WHERE email = ...',
});
}
});
This code leaks:
- Whether an email exists in the system (user enumeration)
- Database table and column names
- Full stack traces with file paths
- Internal query structure
Safe Error Responses
The Error Boundary Pattern
// Define application error types
class AppError extends Error {
constructor(
public statusCode: number,
public userMessage: string, // Safe to show to users
public internalMessage: string, // For logs only
public errorCode: string, // Machine-readable error code
) {
super(internalMessage);
this.name = 'AppError';
}
}
class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(
404,
'The requested resource was not found', // Generic — no details
`${resource} with id ${id} not found`, // Specific — for logs
'RESOURCE_NOT_FOUND'
);
}
}
class AuthenticationError extends AppError {
constructor(reason: string) {
super(
401,
'Invalid credentials', // Same message for all auth failures
`Authentication failed: ${reason}`, // Specific reason in logs
'AUTH_FAILED'
);
}
}
class ValidationError extends AppError {
constructor(details: string) {
super(
400,
'The request contains invalid data',
`Validation failed: ${details}`,
'VALIDATION_ERROR'
);
}
}
Global Error Handler
// Express global error handler — LAST middleware registered
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
// Generate unique error ID for correlation
const errorId = crypto.randomUUID();
if (err instanceof AppError) {
// Known application error — log at appropriate level
logger.warn({
errorId,
errorCode: err.errorCode,
message: err.internalMessage,
path: req.path,
method: req.method,
userId: req.user?.id,
});
return res.status(err.statusCode).json({
error: err.userMessage,
code: err.errorCode,
errorId, // User can reference this for support
});
}
// Unknown error — log full details, return nothing useful
logger.error({
errorId,
message: err.message,
stack: err.stack,
path: req.path,
method: req.method,
userId: req.user?.id,
// DO NOT log request body — it may contain passwords
});
// Generic response — no internal details
return res.status(500).json({
error: 'An internal error occurred',
code: 'INTERNAL_ERROR',
errorId,
});
});
Login Endpoint Done Right
app.post('/api/login', async (req, res, next) => {
try {
const { email, password } = LoginSchema.parse(req.body);
const user = await db.query(
'SELECT id, password_hash, role FROM users WHERE email = $1',
[email]
);
// SAME response for "user not found" and "wrong password"
if (!user.rows.length) {
// Still hash something to prevent timing attacks
await bcrypt.hash('dummy', 10);
throw new AuthenticationError(`No user with email ${email}`);
}
const valid = await bcrypt.compare(password, user.rows[0].password_hash);
if (!valid) {
throw new AuthenticationError(`Wrong password for ${email}`);
}
// Both errors produce identical response: { error: "Invalid credentials" }
const token = generateToken(user.rows[0]);
res.json({ token });
} catch (err) {
next(err);
}
});
Structured Logging Without PII
What NOT to Log
// DANGEROUS: AI logs everything for debugging
logger.info('Login attempt', {
email: req.body.email,
password: req.body.password, // NEVER log passwords
creditCard: req.body.card, // NEVER log payment data
ssn: req.body.ssn, // NEVER log PII identifiers
token: req.headers.authorization, // NEVER log auth tokens
cookie: req.headers.cookie, // NEVER log session cookies
});
Safe Logging Pattern
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
redact: {
paths: [
'req.headers.authorization',
'req.headers.cookie',
'req.body.password',
'req.body.token',
'req.body.creditCard',
'req.body.ssn',
'*.password',
'*.secret',
'*.token',
],
censor: '[REDACTED]',
},
serializers: {
req: (req) => ({
method: req.method,
url: req.url,
// Only log safe headers
headers: {
'content-type': req.headers['content-type'],
'user-agent': req.headers['user-agent'],
'x-request-id': req.headers['x-request-id'],
},
}),
},
});
// Middleware to attach request ID
app.use((req, res, next) => {
req.requestId = req.headers['x-request-id'] || crypto.randomUUID();
res.setHeader('x-request-id', req.requestId);
req.log = logger.child({ requestId: req.requestId });
next();
});
Python Safe Logging
import logging
import re
class PiiFilter(logging.Filter):
PATTERNS = [
(re.compile(r'password["\s:=]+\S+', re.I), 'password=[REDACTED]'),
(re.compile(r'token["\s:=]+\S+', re.I), 'token=[REDACTED]'),
(re.compile(r'Bearer\s+\S+'), 'Bearer [REDACTED]'),
(re.compile(r'\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b'), '[CARD_REDACTED]'),
]
def filter(self, record):
msg = record.getMessage()
for pattern, replacement in self.PATTERNS:
msg = pattern.sub(replacement, msg)
record.msg = msg
record.args = ()
return True
logger = logging.getLogger(__name__)
logger.addFilter(PiiFilter())
Stack Trace Suppression
// Environment-aware error detail
function formatError(err: Error, isProduction: boolean) {
if (isProduction) {
return {
error: 'An internal error occurred',
code: 'INTERNAL_ERROR',
};
}
// Development only
return {
error: err.message,
stack: err.stack,
name: err.name,
};
}
// In Express
const isProduction = process.env.NODE_ENV === 'production';
app.use((err, req, res, next) => {
const status = err.statusCode || 500;
res.status(status).json(formatError(err, isProduction));
});
Debug Endpoint Removal
AI frequently creates debug endpoints and forgets to remove them.
// DANGEROUS: AI-generated debug endpoints that ship to production
app.get('/debug/env', (req, res) => {
res.json(process.env); // Leaks ALL environment variables including secrets
});
app.get('/debug/db', async (req, res) => {
const tables = await db.query("SELECT * FROM information_schema.tables");
res.json(tables.rows); // Leaks entire database schema
});
app.get('/api/test', (req, res) => {
res.json({ status: 'ok', config: app.get('config') }); // Leaks config
});
Prevention
# Search for debug endpoints before deploying
grep -rn "debug\|/test\|/dump\|/env\|information_schema\|process\.env" \
--include="*.ts" --include="*.js" --include="*.py" \
src/ routes/ api/
# CI check: fail if debug patterns exist in production code
// If you need debug endpoints, gate them strictly
if (process.env.NODE_ENV === 'development') {
app.get('/debug/health', debugHealthHandler);
}
// Better: use a separate debug server on a different port
// that is never exposed externally
Version and Server Header Removal
// Express
app.disable('x-powered-by');
// Additional headers to remove or set
app.use((req, res, next) => {
res.removeHeader('X-Powered-By');
res.removeHeader('Server');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
next();
});
// Nginx
server {
server_tokens off; # Removes version from Server header
more_clear_headers Server; # Removes Server header entirely
}
Custom Error Pages
// HTML applications need custom error pages
// AI generates default framework error pages that leak info
// 404 handler
app.use((req, res) => {
if (req.accepts('html')) {
res.status(404).sendFile(path.join(__dirname, 'public', '404.html'));
} else {
res.status(404).json({ error: 'Not found' });
}
});
// 500 handler — never show the default Express error page
app.use((err, req, res, next) => {
logger.error({ err, path: req.path });
if (req.accepts('html')) {
res.status(500).sendFile(path.join(__dirname, 'public', '500.html'));
} else {
res.status(500).json({ error: 'Internal server error' });
}
});
The Error Handling Checklist
| Check | Risk if Missing |
|---|---|
| Stack traces hidden in production | Reveals code structure, file paths, dependencies |
| Same message for all auth failures | User enumeration |
| No PII in logs | Credential exposure, compliance violation |
| Debug endpoints removed | Full system access |
| Server/version headers removed | Targeted exploit selection |
| Error IDs in responses | Users can't reference issues to support |
| Custom error pages | Framework version disclosure |
| Request bodies not logged raw | Password/token leakage in log storage |
Every error message is a potential intelligence report for an attacker. Make your errors helpful for your team (in logs) and useless for attackers (in responses).
Install this skill directly: skilldb add vibe-coding-security-skills