# Server-side code does not log full user objects, bodies, or credentials

- **Pattern:** `ab-002620` (`project-snapshot.legal.no-pii-in-server-logs`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-23
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002620
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002620)

## Why it matters

A `console.log(user)`, `console.log(req.body)`, or `logger.info({ email, password })` inside a route handler ends up durably stored in Vercel Functions logs, CloudWatch, Datadog, Better Stack, or Sentry for days to months — searchable by every engineer, support agent, and third-party log processor with access, and harvestable in any breach of the logging pipeline itself. Slack (2023) leaked employee credentials through Sentry when raw auth objects were captured on exception; the T-Mobile 2023 class action ($350M) explicitly cited logged PII as a breach-scope amplifier. Under GDPR Art. 32 ("security of processing"), logging passwords, tokens, session IDs, or unredacted PII into a shared log pipeline is a processing-security failure whether or not the logs leak — and once they do, Art. 33-34 breach-notification timelines start. AI coding tools default to `console.log(req.body)` and `console.log(user)` during debugging and routinely leave those statements in on ship, because `grep console.log` is not part of the "ready to deploy" checklist that training data reinforces.

## Severity rationale

High because logged credentials are an immediate live credential-theft vector against the logging pipeline itself, and widen any downstream breach scope dramatically — small code change, disproportionate exposure reduction.

## Remediation

Strip every offending log call and replace it with a redacted equivalent. For structured loggers (pino, winston, Sentry), configure a redaction list:

```ts
// lib/logger.ts
import pino from 'pino'
export const logger = pino({
  redact: { paths: ['req.body.password', 'req.body.token', 'user.email', 'user.ssn', '*.authorization'], censor: '[REDACTED]' },
})
```

For plain `console.log`, either delete the statement or replace the payload with a non-PII shape (`console.log('[auth] login attempt', { userId: user.id, ok: true })` — ID only, never the email, token, or body). Deeper coverage of logging hygiene (log-retention limits, log-aggregator encryption, redaction testing) lives in the `data-protection` and `security-hardening` Pro audits.

## Detection

- **ID:** `no-pii-in-server-logs`
- **Severity:** `high`
- **What to look for:** Grep server-side source only: `app/api/**/*.{ts,js}`, `pages/api/**/*.{ts,js}`, `app/**/route.{ts,js}`, `app/**/actions.{ts,js}`, any `.tsx` file without `'use client'`, `lib/**/*.{ts,js}`, `server/**/*.{ts,js}`, `middleware.ts`. Skip `node_modules`, `.next`, `dist`, `build`, `**/*.test.*`, `**/*.spec.*`, `**/__mocks__/*`. Find calls matching any of: `console.(log|error|warn|info|debug)`, `logger.(info|warn|error|debug|trace)`, `pino().*`, `winston.*`, `Sentry.captureException`, `Sentry.captureMessage`, `Sentry.setContext`, `Sentry.setUser` (when the user argument is a full object rather than a sanitized `{ id }`). Then classify each call's argument list. Fail-triggering argument shapes: a bare identifier matching `/^(user|users|account|session|req|request|body|headers|cookies|token|tokens|password|secret|apiKey|api_key|creds|credentials|ssn|phone|address)$/`; a property-access chain ending in one of those (`req.body`, `req.headers`, `req.cookies`, `session.user`); an object literal whose keys include any of `email|password|token|session|apiKey|ssn|phone|address|firstName|lastName|fullName|dateOfBirth|dob`. Non-triggering shapes: a string literal only (`console.log('[auth] login attempt')`), an object with only IDs (`{ userId: 'abc' }`, `{ reqId }`), a redacted payload (`{ email: '[REDACTED]' }`). Count every match with file:line.
- **Pass criteria:** Zero server-side log calls dump a full user object, request body, request headers, session, credentials, or an object literal containing email / password / token / SSN / phone.
- **Fail criteria:** One or more server-side log statements match the fail-triggering argument shapes above.
- **Skip (N/A) when:** The project has no server-side code at all — purely static site with no `app/api/`, no `pages/api/`, no server actions, no `middleware.ts`, no server components importing a database. Quote: `"No server-side code detected — purely static build."`
- **Do NOT pass when:** A log statement wraps the problem identifier in `JSON.stringify` (`console.log(JSON.stringify(req.body))`) — stringifying the object does not strip the PII, it serializes it. Also do not pass on commented-out `// console.log(user)` only if the commented line shipped uncommented anywhere else.
- **Before evaluating, quote:** Quote each matching line with file path and line number, e.g. `"app/api/auth/login/route.ts:17: console.log('login', req.body)"`. If zero matches, state `"0 server-side log statements dump user / body / headers / session / credentials across N server files scanned."`
- **Report even on pass:** `"Scanned <N> server-side files; 0 PII-leaking log calls; <M> sanitized log calls observed (ID-only payloads)."`
- **Detail on fail:** `"app/api/auth/login/route.ts:17 logs full req.body via console.log('login attempt', req.body) — credential exposure to Vercel log pipeline."` or `"app/api/webhook/route.ts:42 calls Sentry.captureException(err, { extra: { user } }) — full user object including email forwarded to Sentry."`
- **Remediation:** Strip every offending log call and replace with a redacted equivalent (see fenced `lib/logger.ts` example in `remediation_prose` above). For plain `console.log`, delete or replace the payload with a non-PII shape. Deeper coverage lives in the `data-protection` and `security-hardening` Pro audits.

## External references

- gdpr Art. 32
- cwe CWE-532

Taxons: project-snapshot, regulatory-conformance

HTML version: https://auditbuffet.com/patterns/ab-002620
