Plaintext passwords are not written to logs or errors
Why it matters
A password written to a log file does not expire, does not rotate, and does not observe the principle of least privilege — it is accessible to everyone who can read that log aggregation service. CWE-312 (Cleartext Storage of Sensitive Information) and CWE-532 (Insertion of Sensitive Information into Log File) both apply directly. PCI-DSS broadly requires protection of authentication credentials at rest and in transit, and OWASP A09 (Security Logging and Monitoring Failures) covers logging too much alongside logging too little. A single console.log(req.body) debug statement in the login handler can write millions of plaintext passwords to your log aggregation service before anyone notices.
Severity rationale
Critical because passwords written to logs persist indefinitely, travel to third-party log services, and expose every affected user's credential regardless of whether the database is ever breached.
Remediation
Destructure the request body before logging so the password variable is never in scope at the log call site:
// Wrong — logs the password
const body = await request.json()
console.log('Login attempt:', body)
// Correct — log only the safe field
const { email, password } = await request.json()
console.log('Login attempt for:', email)
// `password` is never passed to any log call
// For logging middleware, configure redaction:
const logger = pino({
redact: ['body.password', 'body.currentPassword', 'body.newPassword', 'body.token']
})
After fixing the code, review your log aggregation service (Datadog, Logtail, CloudWatch) for historical entries containing password fields. If found, rotate affected accounts and issue security notifications per your incident response plan.
Detection
-
ID:
no-plaintext-passwords-logs -
Severity:
critical -
What to look for: Search for
console.log,console.error,logger.info, and similar logging calls in or near the authentication flow (login, registration, password change). Check if any logging call includesreq.body,request.json()results, or variables namedpassword,credential, or similar without first omitting those keys. Also check error handlers — does the catch block log the full request body when a database error occurs during login? Count all instances found and enumerate each. -
Pass criteria: No logging calls in the auth flow include raw request bodies or password fields. If request logging middleware is used (Morgan, Pino HTTP), it is configured to redact sensitive fields (
password,token,secret). At least 1 implementation must be confirmed. -
Fail criteria: Any
console.log(req.body)or similar that would capture the password in the auth flow. Error handlers that serialize the full request context including POST body to logs. -
Skip (N/A) when: No local password authentication. Signal: OAuth-only auth, managed provider handles passwords.
-
Detail on fail:
"Login handler at /api/auth/login logs console.log(body) at the start of the function — captures the plaintext password on every login attempt"or"Error handler wrapping registration sends full request body to error reporting service including password field". -
Remediation: Passwords written to logs remain there indefinitely and may be shipped to log aggregation services. Remove any logging that could include password fields:
// Wrong: logs the password const body = await request.json() console.log('Login attempt:', body) // Correct: log only safe fields const { email, password } = await request.json() console.log('Login attempt for:', email) // Never log password // If using logging middleware, configure redaction: const logger = pino({ redact: ['body.password', 'body.currentPassword', 'body.newPassword', 'body.token'] })After fixing, review your existing log aggregation service to determine if any passwords have been written and rotate/alert affected accounts if so.
External references
- cwe · CWE-312 — Cleartext Storage of Sensitive Information
- cwe · CWE-532 — Insertion of Sensitive Information into Log File
- owasp:2021 · A09
Taxons
History
- 2026-04-18·v1.0.0·Initial import from saas-authentication·automated