Skip to main content
interlace
Plugin: secure-codingRules

no-pii-in-logs

Prevent personally identifiable information (PII) — emails, SSNs, credit cards, phone numbers — from reaching console / logger output.

Prevent personally identifiable information (PII) — emails, SSNs, credit cards, phone numbers — from reaching console.* or logger output. Logs are routinely shipped to third-party platforms (Datadog, Sentry, CloudWatch) and indexed by people who never had access authorization for that data.

This rule is part of eslint-plugin-secure-coding.

Quick Summary

AspectDetails
SeverityHigh (Security / Compliance — GDPR Art. 32, HIPAA §164.312)
Auto-Fix💡 Suggestions (redact, hash, or replace with stable user id)
CategorySecure Coding
CWECWE-359
Best ForAny service that handles user data and ships logs off-host

Why this matters

Application logs are the highest-volume, lowest-trust egress channel in most production services. By default they are:

  • Shipped to vendors (Datadog, Sentry, Splunk, Loki, ELK) — broadening the trust boundary.
  • Retained for weeks to months — long enough that a breach years from now still discloses today's data.
  • Read by anyone with engineering oncall — not the access-controlled subset that touches the user record itself.

GDPR Article 32 (security of processing) and HIPAA §164.312 (technical safeguards) both treat log-leaked PII as a reportable incident. Most breach disclosures in 2024–2025 traced the data exposure to a logger, not a database.

What the rule detects

The rule flags PII patterns whether they enter the log call as a literal, a templated string, or a member access on an user/person/account identifier:

PatternDetected as
Hard-coded email regex matchemail
user.email, account.ssninferred via identifier shape
JSON.stringify(user)a whole person record, serialized
+user.phone, user.ssn * 1a numeric conversion keeps the value
13–19 digit number passing Luhncredit-card
US SSN (\d{3}-\d{2}-\d{4})ssn

The sinks are console.log / error / warn / info (configurable through consoleMethods) and every level method — trace, debug, verbose, info, log, warn, error, fatal — of a logger receiver: logger.info(…), log.warn(…), this.logger.error(…) (receiver names configurable through loggers).

Examples

❌ Incorrect

console.log('user signed in', user.email, user.phone);
logger.info(`payment from ${customer.creditCard}`);
console.error('lookup failed for SSN', record.ssn);

// PII concealed in JSON.stringify still gets logged
logger.debug(JSON.stringify(user));

✅ Correct

import { redactPii, hashStable } from '@/lib/pii';

console.log('user signed in', { userId: user.id });
logger.info('payment authorised', { customerId: hashStable(customer.id) });
console.error('lookup failed', { ssnHash: hashStable(record.ssn) });

// Or use a structured logger that redacts known PII fields by allowlist
logger.debug({ user: redactPii(user) });

Error Message Format

🔒 SECURE-CODING CWE-359 | PII detected in log argument | HIGH
   Fix: Replace the PII payload with a stable opaque id (UUID, hash) or pass through a redacting logger before emission.

Known False Negatives

  • Custom logger wrappers called as bare functions (logEvent(…), audit(…), track(…)) are not sinks. A logger OBJECT under another name is added through the additionalLoggers option; receivers not on the list are skipped.
  • A hard-coded phone number in a log line is not detected: phone formats overlap with order numbers and support lines too much to judge from text.
  • PII reached via aliasing (const e = user.email; log(e);) is tracked one assignment hop; deeper chains require type-aware enabling.
  • PII embedded inside an HTTP request body that incidentally surfaces in an error message (e.g. JSON.stringify(req.body) in a catch block) is not always reached without type info.

⚙️ Options

OptionTypeDefaultDescription
consoleMethodsstring[]["log","error","warn","info"]console methods treated as a log sink, matched as an exact method name. Replaces the built-in list.
additionalConsoleMethodsstring[][]Extra console methods, on top of consoleMethods.
piiTermsstring[]["email","ssn","password","credit card","phone"]Field vocabulary, matched as WHOLE WORDS of an identifier — never as a substring. Multi-word terms such as "credit card" match consecutive segments. Replaces the built-in list.
additionalPiiTermsstring[][]Extra PII field terms, on top of piiTerms.
piiLabelsstring[]["email:","ssn:","password:","credit card:","creditcard:"]String literals that are EXACTLY a field label announcing the next argument. Compared case-insensitively after trimming. Replaces the built-in list.
additionalPiiLabelsstring[][]Extra field labels, on top of piiLabels.
loggersstring[]["logger","log"]Receiver names that denote an application logger (logger.info, this.logger.error); every level method on one is a log sink. Replaces the built-in list.
additionalLoggersstring[][]Extra logger receiver names, on top of loggers.
piiRecordsstring[]["user","person","customer","account","applicant","patient"]Words naming a whole person record, matched as WHOLE WORDS of an identifier. JSON.stringify of one is reported. Replaces the built-in list.
additionalPiiRecordsstring[][]Extra person-record words, on top of piiRecords.

Use this rule

npm install -D oxlint-plugin-secure-coding
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-secure-coding"],  "rules": {    "secure-coding/no-pii-in-logs": "error"  }}
vite.config.ts
import { defineConfig } from 'vite-plus';export default defineConfig({  lint: {    jsPlugins: ['oxlint-plugin-secure-coding'],    rules: {      'secure-coding/no-pii-in-logs': 'error',    },  },});

oxlint-plugin-secure-coding re-exports eslint-plugin-secure-coding/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.