Error handlers that serialize console.error(err, req) or JSON.stringify(error) inadvertently capture Authorization headers, database connection strings from caught exceptions, and environment variable snapshots. CWE-532 (Insertion of Sensitive Information into Log File) and OWASP A09 (Security Logging and Monitoring Failures) cover this. NIST 800-53 AU-3 requires log content to not expose sensitive data. Logs shipped to a third-party service (Datadog, Logtail, Axiom) mean those credentials are now in an external system with its own access controls, multiplying the exposure surface. A single console.error(process.env) in an error handler is a complete secrets exfiltration.
Info because secret leakage into logs requires the logs to be accessed by an attacker, but log storage is often less tightly controlled than environment variable management.
Implement a logError function that redacts sensitive keys before writing to any log sink:
// lib/logger.ts
const SENSITIVE = new Set(['password', 'token', 'secret', 'authorization', 'cookie', 'key'])
function redact(obj: unknown, depth = 0): unknown {
if (depth > 5 || obj === null || typeof obj !== 'object') return obj
return Object.fromEntries(
Object.entries(obj as Record<string, unknown>).map(([k, v]) => [
k,
SENSITIVE.has(k.toLowerCase()) ? '[REDACTED]' : redact(v, depth + 1),
])
)
}
export function logError(err: unknown, ctx?: Record<string, unknown>) {
console.error(JSON.stringify({
level: 'error',
message: err instanceof Error ? err.message : String(err),
context: ctx ? redact(ctx) : undefined,
timestamp: new Date().toISOString(),
}))
}
Never pass req or process.env directly to a logger. Pass only the specific context fields the error needs.
ID: security-hardening.infra-monitoring.secrets-not-in-logs
Severity: info
What to look for: List all logging statements in the codebase. For each, check whether any secrets, tokens, or passwords could be logged. check error handlers and logging calls. Look for patterns that might serialize the entire request object (including headers with Authorization tokens), log database connection strings from caught exceptions, or include environment variables in error responses. Search for console.log(req), console.log(error), JSON.stringify(error) — these can inadvertently capture sensitive values.
Pass criteria: Error handlers log the error message and stack trace but not the full request headers, body, or environment context. Sensitive fields are redacted from any structured logging. The error response returned to the client contains no sensitive information — 0% of log statements should contain unredacted secrets. Report: "X logging statements scanned, 0 expose secrets."
Fail criteria: Error handlers serialize and log the full request object (including Authorization headers). Caught exceptions that include connection strings are logged verbatim. Error responses include stack traces or query strings.
Skip (N/A) when: Never — secret protection in logs applies universally.
Detail on fail: "Global error handler logs console.error(err, req) — Authorization headers and request bodies captured in logs" or "Database connection errors logged verbatim — connection string with credentials appears in application logs"
Remediation: Implement redacted structured logging:
// lib/logger.ts
const SENSITIVE_KEYS = new Set(['password', 'token', 'secret', 'authorization', 'cookie', 'key'])
function redact(obj: unknown, depth = 0): unknown {
if (depth > 5 || obj === null || typeof obj !== 'object') return obj
return Object.fromEntries(
Object.entries(obj as Record<string, unknown>).map(([k, v]) => [
k,
SENSITIVE_KEYS.has(k.toLowerCase()) ? '[REDACTED]' : redact(v, depth + 1),
])
)
}
export function logError(error: unknown, context?: Record<string, unknown>) {
const safeContext = context ? redact(context) : undefined
console.error(JSON.stringify({
level: 'error',
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
context: safeContext,
timestamp: new Date().toISOString(),
}))
}