Edge and WAF

WAF audit logs without leaking credentials, the secure way

The WAF blocked a login attack and wrote a perfect record of it: the user's password, their session cookie and a bearer token, now in a log index that forty people can search. Your WAF just became the best place to steal credentials from.

The short answer

Log audit parts A, F, H and Z only: drop B (request headers, which hold Authorization and Cookie) and C or I (request bodies, which hold passwords). Use SecAuditEngine RelevantOnly. The rule match line still quotes the matched value, so redact sensitive parameter names in the log shipper before the log leaves the node.

Updated Houssam Hammoudi, CTOTested with coraza-proxy-wasm 0.6.0 (Coraza 3.3.3, OWASP CRS 4.14.0), Envoy 1.39.1

On this page
  1. What goes wrong
  2. What the docs say
  3. The secure configuration
  4. Prove it
  5. Mistakes people make
  6. Checklist

What goes wrong

A WAF audit log records a whole transaction, split into parts named by letters. Part B is the request headers. Part C is the request body. They are the parts that make an audit log useful for an investigation, and they are exactly where credentials live: Authorization: Bearer ..., Cookie: session=..., and password=... in a login form.

Coraza's default parts are ABCFHZ. The recommended config that ships with coraza-proxy-wasm sets ABIJDEFHZ. Both include request headers and body. The first time an attack hits your login endpoint, the log stores every credential in the request, in plain text, and ships it to wherever your logs go.

There is a second path. When a CRS rule matches, the error log line quotes the value it matched, in a [data "Matched Data: ... found within ARGS_POST:password: ..."] field. That line is written no matter which audit parts you choose.

What the docs say

B: Request headers. C: Request body (present only if the request body exists and Coraza is configured to intercept it. This would require SecRequestBodyAccess to be set to on).

Source: Coraza docs, SecAuditLogParts

Default: ABCFHZ

Source: Coraza docs, SecAuditLogParts

Coraza's list of actions has no sanitiseArg or sanitiseMatched, the ModSecurity v2 actions that masked values in logs; see the Coraza actions reference. The docs do not warn that parts B and C contain credentials, and nothing mentions the matched-data field at all.

The secure configuration

Coraza directives (in coraza-proxy-wasm, entries in directives_map):

conf
Include @recommended-conf
SecRuleEngine On
# Write an audit record only for relevant transactions (rule matches and
# the relevant status codes, which @recommended-conf sets to 40x/41x and
# 50x/51x), not for every request.
SecAuditEngine RelevantOnly
# A = header (time, client, transaction id), F = response headers,
# H = trailer (engine state), Z = end. No B (request headers: Authorization,
# Cookie), no C/I (request body: passwords, tokens), no E (response body).
SecAuditLogParts AFHZ
SecAuditLogFormat JSON
Include @crs-setup-conf
Include @owasp_crs/*.conf

The transaction id in part A matches the unique_id in the rule match lines, so you can still join an audit record to the rules that fired.

Redaction for the rule match lines, in the log shipper, before anything leaves the node. As a sed expression (translate it to your shipper's replace stage: the replace function in Vector, stage.replace in Grafana Alloy, or a Lua filter in Fluent Bit):

bash
# Replace the value after "found within <COLLECTION>:<name>: " when the
# name looks like a credential. Case-insensitive.
sed -E 's/(found within [A-Z_]+:[^:"]*(pass|pwd|secret|token|key|auth|session|cookie)[^:"]*: )[^"]*/\1[REDACTED]/I'

In the Envoy access log, never log %REQ(AUTHORIZATION)%, %REQ(COOKIE)% or the full query string of login and token endpoints.

Prove it

The test in secure-tests/waf-audit-logs-without-credentials/run.sh sends one login attempt that trips CRS rule 942100: a bearer token, a session cookie, and a password with SQL in it.

bash
curl -X POST https://app.example.com/login \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.c2VjcmV0.sig" \
  -H "Cookie: session=s3cr3t-session-id" \
  --data "user=alice&password=hunter2%27+OR+%271%27%3D%271"

The fields found in the JSON audit record, with Coraza's default parts and with AFHZ:

text
== SecAuditLogParts ABCFHZ
POST /login                                       -> 403
audit log fields:
  "authorization":["Bearer eyJhbGciOiJIUzI1NiJ9.c2VjcmV0.sig"]
  "cookie":["session=s3cr3t-session-id"]
  "body":"user=alice&password=hunter2%27+OR+%271%27%3D%271"

== SecAuditLogParts AFHZ
POST /login                                       -> 403
audit log fields:
  "body":""
  "headers":null

The rule match line, which does not depend on the audit parts, and the same line after the redaction filter:

text
== The error log line for rule 942100 (any audit parts)
  [data "Matched Data: s&sos found within ARGS_POST:password: hunter2' OR '1'='1"]

== The same line after the shipper's redaction filter
  [data "Matched Data: s&sos found within ARGS_POST:password: [REDACTED]"]

Mistakes people make

ABIJDEFHZ is built for debugging a rule set, not for production logs that many people can read. Choose parts on purpose.

Relying on the log backend's access control

Logs get copied: to a SIEM, to a support ticket, to a laptop during an incident. A credential that was never written cannot leak from any of them.

Fixing the audit log and forgetting the match lines

The [data "Matched Data: ..."] field quotes the matched variable. It shows up in the proxy log on every rule match, with or without an audit log.

Removing inspection of the password field instead

Excluding ARGS:password from the rules removes the leak and the protection at the same time. Redact the log, keep the inspection.

Logging everything to catch everything

SecAuditEngine On writes a record for every request. With parts B and C that is a full copy of every login. Use RelevantOnly.

Checklist

  • Set SecAuditLogParts AFHZ (no B, C, I, E).
  • Set SecAuditEngine RelevantOnly.
  • Add a redaction stage for matched-data values of credential-like names.
  • Keep Authorization, Cookie and login query strings out of the access log.
  • Send one test login attack and grep the logs for its password and token.
  • Set a retention period for WAF logs and restrict who can search them.

A good WAF log tells you who attacked, when and with what. It should never be able to tell you the password.

H2-CPQE

Learn it on a live range

WAF, rate limiting and DDoS, in Edge and Post-Quantum Networking: a real host in your browser, and every objective checked on the machine.

Start free

The Dome

Want it run for you?

The Dome puts post-quantum TLS, a WAF that blocks, signed DNS and a zero-trust mesh in front of your application. Tell us what you run.

See the Dome