un

guest
1 / ?
back to lessons

Headers as Bags

HTTP logging frameworks treat request headers as a bag of key-value pairs. The logging API exposes the full bag. Operators enable header logging for debugging: when a request fails, the headers tell the story. No built-in denylist. No credential filtering in the documentation. Full headers to disk.

The credential headers in a typical request:

- Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... (JWT or OAuth token)

- Cookie: session=abc123; auth=xyz789

- X-API-Key: sk-live-abc123...

- X-Auth-Token: ghp_abc123... (GitHub personal access token pattern)

These values authenticate the request. Written to a log file, they authenticate any request.

The Credential Pipeline

A credential written to a log file does not stay in one place. It travels:

1. Web server writes to /var/log/nginx/access.log

2. Log rotation agent (logrotate) copies to /var/log/nginx/access.log.1

3. Log shipper (Fluentd, Filebeat, Logstash) reads & ships to aggregator

4. Log aggregator (Elasticsearch, Splunk, Datadog) indexes & stores

5. Retained for 30-90 days under default policy

The credential exists in all five locations simultaneously. Revoking the session token does not remove the credential from the log aggregator. It remains searchable, exportable, & accessible to anyone with log access for the full retention window.

The Exposure Window

Exposure window for a credential in memory: max(session duration, process lifetime). Session: hours to days. Process: hours to weeks.

Exposure window for a credential in a log: max(session duration, log retention). Session: hours to days. Retention: 30-90 days.

A credential stolen from memory required the attacker to be present during the session window. A credential stolen from a log requires only access to the log aggregator, available retroactively, for the full retention period.

MOAD-0003 vs MOAD-0004

MOAD-0003 (Leaked Context): a credential in memory leaks to the wrong request handler. Accessible only during the process window, through the thread pool. Ephemeral.

MOAD-0004 (Logged Secret): a credential on disk persists through log rotation, log shipping, & log aggregation. Accessible retroactively, to anyone with log access, for 30-90 days. Persistent.

The structural difference: ephemeral vs persistent. The fix operates at a different layer.

Ephemeral vs Persistent

The ephemeral/persistent distinction determines the risk surface, the fix layer, & the incident response requirements.

Why does MOAD-0004 carry higher risk than MOAD-0003? Compare where each credential lives and for how long.

Credential Denylist at the Serialization Layer

The fix: a credential denylist at the serialization layer. Before any header value reaches the log output, check the header name against a denylist. Replace the value with [REDACTED].

CREDENTIAL_HEADERS = {
    'authorization',
    'cookie',
    'x-api-key',
    'x-auth-token',
    'x-csrf-token',
    'proxy-authorization',
}

def sanitize_headers(headers: dict) -> dict:
    return {
        k: '[REDACTED]' if k.lower() in CREDENTIAL_HEADERS else v
        for k, v in headers.items()
    }

The denylist belongs at the serialization layer, not at the log query layer. Log query redaction: applies after the credential reached disk; the raw value still exists, just hidden from display. Serialization layer redaction: the credential never reaches disk. The raw value never enters the log file, the log shipper, or the log aggregator.

Testing the Denylist

Three test patterns:

- Positive: a request with Authorization: Bearer token123 produces a log entry with Authorization: [REDACTED]

- Negative: a request with Content-Type: application/json produces a log entry with the value intact

- Case-insensitive: AUTHORIZATION: Bearer token123 also produces [REDACTED] (HTTP header names case-insensitive)

The denylist requires maintenance: new credential header patterns (e.g., custom X-Service-Auth headers) need explicit addition. The fix is structural but not self-maintaining.

Apply the Denylist

A team configures their Nginx access log format to include all request headers for debugging a production incident. The configuration:

log_format debug_format '$remote_addr - $request - $http_authorization - $http_cookie';
access_log /var/log/nginx/debug.log debug_format;

They resolve the incident & intend to remove the debug configuration, but the change does not reach production before the next deployment cycle (7 days later).

Identify the defect. What credential headers could be exposed? Describe the denylist approach: where does it apply, what does it check, and what does it produce?