Lab 06 — HTTP Request Smuggling: The Parser-Differential Engine
Intensity: advanced. This lab builds the analytical core of one of the most consequential and subtle classes of web vulnerability — HTTP request smuggling. You will model two HTTP processors as policies, compute how each frames a message, and detect the differential that lets an attacker hide one request inside another. Then you prove the fix: a single strict policy on both hops makes desync impossible.
Prerequisites: Phase 01 (parser differentials), Phase 02 WARMUP Chapter 3 (HTTP framing) and Chapter 6 (parser differentials), Chapter 11 (the attack/defense map). Authorized-lab only — this analyzes header metadata; it does not send traffic anywhere.
Table of Contents
- 1. The Concept, From Zero
- 2. Why Two Parsers Disagree
- 3. The Four Desync Classes
- 4. What the Attack Actually Achieves
- 5. Threat Model
- 6. Architecture of This Lab
- 7. Implementation Steps
- 8. Running and Expected Evidence
- 9. The Fix and Why It Works
- 10. Hardening Tasks (Stretch)
- 11. Common Mistakes
- 12. Interview Questions
- 13. Resume Bullet
- 14. References
1. The Concept, From Zero
What is HTTP framing? HTTP/1.1 runs many requests over one reused TCP connection (keep-alive). The bytes on that connection are a continuous stream; there is no packet boundary that says "this request ends here." So every HTTP processor must compute, from the headers alone, where the body of the current message ends and the next message begins. There are exactly two mechanisms:
Content-Length: N— the body is the next N bytes, then a new message starts.Transfer-Encoding: chunked— the body is a sequence of size-prefixed chunks, each<hex-size>\r\n<data>\r\n, terminated by a zero-size chunk0\r\n\r\n. The body ends at the terminating chunk; the next byte starts a new message.
Where the danger comes from. Production traffic almost never hits one server directly. It traverses a chain: a CDN, a load balancer, a reverse proxy / WAF, and finally the application server. Each hop independently re-parses the byte stream to find message boundaries. If two hops compute a different boundary for the same bytes, the stream is "desynchronized" — and an attacker can craft a request whose tail one hop treats as body (harmless) while the next hop treats it as the start of a new request (attacker-controlled, and already past the first hop's security checks).
This is request smuggling, and it is a parser differential (Phase 01, Chapter 6) operating at the HTTP message layer. It is the same root cause as the TLV duplicate-field bug from Phase 01 — two components disagree about where a thing begins or ends — but the consequences are enormous because the front hop is usually the one enforcing authentication, routing, and WAF rules.
2. Why Two Parsers Disagree
Three ambiguities, all addressed (and forbidden) by RFC 9112 but tolerated by many real deployments:
- Both
Content-LengthandTransfer-Encodingpresent. The spec says Transfer-Encoding wins and the message must be treated as suspect. But a permissive server might honor theContent-Lengthinstead. If the front uses CL and the back uses TE (or vice-versa), they frame the message differently → desync. - Duplicate / conflicting
Content-Length.Content-Length: 10andContent-Length: 20. One hop takes the first, another takes the last. - Obfuscated
Transfer-Encoding.Transfer-Encoding: xchunked,Transfer-Encoding: chunked\t,Transfer-Encoding: chunked, identity, or a header whose name has subtle whitespace. A strict parser rejects these as "not chunked"; a lenient parser is fooled into treating them as chunked. If only one of the two hops is fooled, they disagree.
The lab models a processor with a Policy:
Policy(name, reject_both, reject_dup_cl, strict_te_token, prefer)
reject_both— refuse a message with both CL and TE (the strict, correct behavior).reject_dup_cl— refuse conflicting Content-Length values.strict_te_token— only the exact tokenchunkedcounts as chunked (no obfuscation).prefer— when both are present and the processor doesn't reject:"te"or"cl".
3. The Four Desync Classes
Naming convention: the vector is named <front-belief>.<back-belief>.
| Class | Front-end frames by | Back-end frames by | How it arises |
|---|---|---|---|
| CL.TE | Content-Length | chunked | front honors CL, back honors TE (or back is fooled by obfuscation into chunked) |
| TE.CL | chunked | Content-Length | front honors TE, back honors CL |
| TE.TE | chunked | chunked | both support TE but one is fooled by an obfuscated TE the other ignores |
| CL.CL | Content-Length (value A) | Content-Length (value B) | duplicate Content-Length; hops pick different copies |
In a CL.TE attack, the front-end reads Content-Length bytes as the body and forwards
the whole thing; the back-end reads the chunked body, finishes at the zero-chunk early,
and treats the leftover bytes as a new request it parses on the same connection. In
TE.CL, the roles reverse. The attacker chooses the variant that matches the deployment.
4. What the Attack Actually Achieves
Smuggling is not the payoff; it is the primitive. With a reliable desync an attacker can:
- Bypass front-end security controls — the smuggled request never passed the WAF / auth / routing on the front hop, so it reaches the back-end as if internal.
- Poison the response queue — the smuggled request's response is delivered to the next legitimate user on the connection, leaking their data or serving attacker content.
- Cache poisoning / web cache deception — desync the cache key from the served content.
- Credential / session theft — capture another user's request because the back-end appended it to the attacker's smuggled prefix.
Because the front hop is usually the trust boundary (Phase 02, Chapter 1 — evidence from one layer cannot substitute for another), smuggling lets an attacker speak to the back-end with the front-end's implied trust. That is why it is consistently rated high or critical.
5. Threat Model
Assets : back-end trust in the front-end's parsing; other users' requests/responses
Actors : an unauthenticated remote attacker sending a single crafted request
Trust boundary: front-end (auth/WAF/routing) -> back-end (application)
Invariant : every hop on the path computes the SAME message boundary for the same bytes
Attack : craft headers so two hops disagree -> smuggle a second request past the front
Demonstrated : a Policy pair that produces a non-"safe", non-"rejected" classification
Out of scope : sending real traffic; we reason over header metadata only
6. Architecture of This Lab
headers (Content-Length values, Transfer-Encoding values)
│
▼
interpret(front_policy, …) ──▶ Outcome(mode, size, reason) # how the FRONT frames it
interpret(back_policy, …) ──▶ Outcome(mode, size, reason) # how the BACK frames it
│
▼
detect_differential(front, back, …) ──▶ "safe" | "rejected" | "CL.TE" | "TE.CL" | "TE.TE" | "CL.CL"
interpret is the framing engine for one processor. detect_differential runs it for
both hops and classifies the disagreement. is_vulnerable sweeps a list of attack cases
and returns whether any desync exists for a given hop pair — the function you would run
in CI against your real proxy/server policies.
7. Implementation Steps
Edit lab.py. Implement three functions (the README sections above give the full rules):
_looks_chunked(value, strict)— the obfuscation detector. Strict mode accepts only the exact stripped/lowercased tokenchunked; lenient mode accepts any value containingchunked. This single function is where TE.TE attacks live.interpret(policy, content_length, transfer_encoding)— deny-by-default framing: reject duplicate CL (if configured), reject CL+TE together (if configured), otherwise resolve toCHUNKEDorLENGTH. Return anOutcomewith areasonstring (used as evidence).detect_differential(front, back, …)— frame under both policies and classify. If either side fails closed (REJECT), the ambiguous message never desyncs the pair — return"rejected". Otherwise compare modes/sizes and name the vector.is_vulnerable(front, back, cases)—Trueif any case yields a real desync.
Keep the dataclasses and signatures unchanged — the tests import them by name.
8. Running and Expected Evidence
LAB_MODULE=solution pytest -q # reference implementation — all tests pass
pytest -q # your implementation
The test suite is the evidence package. It proves:
- the obfuscation detector behaves differently in strict vs lenient mode;
- each of the four desync classes (CL.TE, TE.CL, TE.TE, CL.CL) is produced by a realistic hop pair;
- a strict processor fails closed on every ambiguous input;
- two strict processors can never desync (the fix) —
is_vulnerable(STRICT, STRICT, …)isFalseacross the whole adversarial corpus; - a lenient pair is vulnerable.
9. The Fix and Why It Works
Request smuggling is a coordination failure, not a single-component bug. The durable fix
is to make every hop on the path enforce one strict, RFC-9112-aligned framing policy
(the lab's STRICT): reject messages that carry both CL and TE, reject duplicate
Content-Length, and accept only an exact chunked token. When both hops reject the same
ambiguous inputs, there is no ambiguous message that survives to be interpreted two ways
— the differential is structurally impossible, which the final test proves over the entire
attack corpus.
Operationally that means: prefer HTTP/2 end-to-end (its binary framing removes this class), normalize/reject ambiguous HTTP/1.1 at the edge, and ensure the proxy and origin agree on connection reuse. Note the parallel to Phase 01: the cure for a parser differential is always one strict policy enforced everywhere, not lenient "best-effort" parsing.
10. Hardening Tasks (Stretch)
- Extend
interpretto model chunk-size obfuscation (negative, oversized, or non-hex chunk sizes) as additional reject reasons. - Add a header-name normalization stage (leading whitespace, casing, embedded NUL) and show how a name-level obfuscation produces a differential a value-only check misses.
- Add an HTTP/2 downgrade model: a front-end speaking h2 to clients but HTTP/1.1 to the origin can re-introduce smuggling via header translation (h2.CL / h2.TE).
- Emit a decision log record per analysis (
front_reason,back_reason,verdict) with no request bodies — the redaction discipline from Phase 02's logging standard.
11. Common Mistakes
- Classifying by the back-end's belief instead of the front's. The vector name is
front.back; getting the order wrong inverts CL.TE and TE.CL. - Treating "rejected" as "vulnerable." If either hop fails closed, the desync never reaches the back-end — that is the safe outcome, not a finding.
- Checking only the TE value and missing name-level obfuscation (a hardening task).
- "We have a WAF, so we're safe." Smuggling specifically bypasses the front-end WAF; the WAF is the thing being evaded.
- Fixing only one hop. A strict back-end behind a lenient front-end can still be desynced; the policy must be uniform.
12. Interview Questions
- Explain CL.TE vs TE.CL request smuggling. Front and back disagree on body framing; in CL.TE the front uses Content-Length and the back uses chunked, so the back finishes the body early and reads the remainder as a new request (and vice-versa for TE.CL).
- Why is request smuggling usually high/critical even without direct data access? It bypasses front-end auth/WAF/routing and can poison the response queue to hijack other users' requests/responses on the shared connection.
- RFC 9112 says a message with both CL and TE should be handled how? Transfer-Encoding takes precedence and the Content-Length must be treated as an error; a strict server rejects the message.
- What's the durable fix? One strict framing policy on every hop (or HTTP/2 end-to-end); reject ambiguous messages at the edge. Fixing a single hop is insufficient.
- How is this the same bug class as a TLV duplicate-field vulnerability? Both are parser differentials — two components disagree about boundaries/duplicates — and both are cured by strict, uniform parsing rather than lenient acceptance.
13. Resume Bullet
Built a request-smuggling parser-differential analyzer that models HTTP/1.1 framing policies across proxy/origin hops, classifies CL.TE / TE.CL / TE.TE / CL.CL desyncs, and proves that a uniform strict (RFC 9112) policy eliminates the differential across an adversarial corpus.
14. References
- RFC 9112 §6 (HTTP/1.1 message body length) and §7 (Transfer-Encoding).
- James Kettle (PortSwigger), "HTTP Desync Attacks: Request Smuggling Reborn" and the Web Security Academy request-smuggling labs.
- RFC 9113 (HTTP/2) — how binary framing removes this class; h2 downgrade research.
- Phase 01 WARMUP Chapter 6 (parser differentials); Phase 02 WARMUP Chapters 3, 6, 11.