Skip to content

CWE-532: Insertion of Sensitive Information into Log File

Overview

Sensitive information in log files occurs when applications write confidential data (passwords, tokens, PII, session IDs) to application logs, system logs, or debug output. Common sources include authentication-failure logging that captures the attempted password, request/response logging that captures full HTTP bodies, error messages and stack traces containing credentials, verbose debug output left enabled in production, and database query logs showing sensitive parameter values.

Relationship to Other CWEs

CWE-532 has a different parent depending on which MITRE view you read, and both are worth knowing because they answer different questions. Either parent is correct and the remediation does not depend on which.

OWASP Classification

A09:2025 - Security Logging & Alerting Failures

Risk

High: Passwords, API keys and tokens in a log are usable directly by anyone who reads it. Session IDs in access logs allow account takeover. PII and PHI in logs carry regulatory exposure under GDPR and HIPAA, and card numbers violate PCI DSS. Logs are typically readable by a wide audience - administrators, support staff, log aggregation services, backup systems, and anyone who compromises the server - and are retained far longer than the sensitive data's original context. Logs are also frequently overlooked in security assessments.

Remediation Steps

Core Principle: Never log sensitive information; redact at the source and treat logs as a durable, widely-accessible output, not an internal-only debugging artifact.

Locate Sensitive Information in Log Calls

  • Identify log sources: authentication logging, request/response logging, error logging, debug output
  • Check what's actually being logged: passwords, tokens, API keys, PII, session IDs, payment data, health data
  • Trace every logging call site (logger.info(), console.log(), syslog(), and framework-equivalent calls) that touches request data, exception objects, or user-supplied structures directly

Sanitize at the Source (Primary Defense)

// VULNERABLE - logs the raw structure, sensitive fields and all
logger.info(user_data)   // {username: 'alice', password: 'secret123', email: '...'}

// SECURE - a filter redacts known-sensitive field names before the record is written
function normalize(key):
    return lowercase(remove_all(key, ['_', '-', ' ']))

// The list is normalized once at construction, so one entry covers every
// spelling of the name: apiKey, api_key, API-KEY and ApiKey all match 'apikey'
SENSITIVE_FIELDS = { normalize(name) for name in
                     ['password', 'token', 'api_key', 'ssn',
                      'credit_card', 'secret', 'authorization'] }

function redact(data):
    if is_list(data):
        return [ redact(item) for item in data ]     // lists hold objects too
    if not is_object(data):
        return data
    return { key: (REDACTED if normalize(key) in SENSITIVE_FIELDS else redact(value))
             for key, value in data.items() }

logger.add_filter(redact)
logger.info(user_data)   // {username: 'alice', password: '***REDACTED***', email: '...'}

Why this works: A field-name-based filter intercepts every log record before it's written and redacts known-sensitive keys, recursing through nested objects and through lists of objects. It is applied once at the logging layer, so it fails safe against the common case: a call site that forgets to sanitize its own arguments.

It does not fail safe against a field name nobody put on the list, and that is the failure mode to design around. Two details do most of the work.

Normalize the list at construction, as above, rather than normalizing only the incoming key at comparison time. A list written in snake_case and tested against a lowercased key silently never matches the camelCase spelling of its own entries: normalize('apiKey') is apikey, which is not the string api_key, so apiKey and creditCard pass through in full while the list looks complete. Running the list through the same function that processes the key makes that bug unrepresentable. It is worth doing deliberately, because where the list is a plain literal the bug is invisible in testing: anything with a recognisable shape gets caught by the pattern layer below, and only the opaque values (an API key, a bearer token) actually escape.

Recurse into lists as well as objects. A request body of {items: [{credit_card: '...'}]} is an object holding a list holding objects, and a filter that descends only into objects returns that list untouched. Then add the pattern-based layer below, which is what catches the names the list was never going to have.

Add Pattern-Based Redaction as Defense in Depth

Field-name filtering misses sensitive data embedded in free-form text - an error message that happens to include a credit card number, or a concatenated log line. Pattern-based redaction (regex matching for credit-card-shaped digit sequences, SSN formats, API-key-shaped tokens, email addresses) catches these regardless of field name, at the cost of being less precise. Use field-based filtering as the primary control and pattern matching as the layer behind it.

Configure Log Levels for Production

Disable DEBUG/TRACE logging in production - these levels are the most likely to contain full request/response bodies, raw variable dumps, or SQL parameter values. Keep INFO limited to intentional business and audit events rather than full payloads, and use WARN/ERROR for failures and security-relevant events. Full exception detail, still redacted of credentials, belongs in logs restricted to authorized personnel rather than in a response to the caller.

Restrict and Monitor Log Access

  • Restrict log file permissions to authorized processes/users only
  • Apply a retention policy rather than keeping logs indefinitely; anything that slipped through redaction stays exposed for as long as the log does
  • Encrypt logs at rest and in transit to centralized aggregation systems
  • Audit who has access to the log aggregation service itself (Splunk, ELK, Datadog, or equivalent) - it's often a broader audience than production system access

Test the Fix

  • Trigger a failed login with a real-looking password and confirm the password doesn't appear in logs
  • Submit a request containing a credit-card-shaped value and confirm it's redacted, not just the field it arrived in
  • Trigger an error/exception path and confirm the stack trace doesn't include credentials or tokens
  • Search existing production logs for known sensitive-data patterns as a one-time cleanup check, not just testing the code path going forward
  • Re-scan with the security scanner to confirm the finding is resolved

Common Vulnerable Patterns

// VULNERABLE - pseudo-code
logger.info('Login attempt: user=' + username + ', password=' + password)
logger.debug('Request body: ' + json_stringify(request.body))   // may contain card numbers, tokens, PII
except Exception as e:
    logger.error('Access denied for user ' + username + ' using password: ' + password)  // never log the credential, even in an error path

Why this is vulnerable: each line writes a credential into a system built for retention and breadth of access. A log entry outlives the request that produced it by the retention period rather than the session, so a token valid for an hour sits in an index for a year, readable by the whole audience the Risk section describes.

The second and third lines are the ones that keep recurring. Logging a whole request body captures whatever the body holds today and whatever anyone adds to it later, so the exposure grows without this line being edited: a card number added to the payload next quarter is logged from the moment it exists. The error path is where this is most likely, because when something fails the instinct is to record more context, so the log statement written under pressure is the one carrying the password. Log an identifier for the credential rather than the credential - a user id, a token prefix, a hash - so the entry stays useful for diagnosis without being usable for authentication.

Secure Patterns

// SECURE - pseudo-code
logger.info('Login attempt: user=' + username)   // no password in the message at all
logger.debug('Request received', redact(request.body))   // filtered before it reaches the sink
except Exception as e:
    logger.error('Authentication failed', context = { user: username })   // no credential, ever

Why this works: The secure versions never pass the secret to the logger, so there is nothing for redaction to remove afterwards. Where structured data must be logged wholesale (the request-body case), it goes through the redaction filter before reaching the log sink.

Additional Resources