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.
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.
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.
ID: saas-authentication.password-credential.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 includes req.body, request.json() results, or variables named password, 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.