HTTP smuggling — modern variants
TL;DR: Classic CL/TE/TE.CL smuggling (HTTP/1.1) is mostly mitigated at major CDNs; newer variants ride (1) HTTP/2 frame parsing mismatches, (2) HTTP/2 to HTTP/1.1 downgrade desync, (3) header-value injection through whitespace/CR-LF, (4) browser-based client-side desync, (5) HTTP/3’s QPACK state. James Kettle’s “HTTP/2 The Sequel is Always Worse” and “Browser-Powered Desync” research is the canonical reference. Companion to http-request-smuggling and http3-quic-attack-surface.
Classic recap
In HTTP/1.1, both Content-Length (CL) and Transfer-Encoding: chunked (TE) declare body framing. A proxy parsing one and origin parsing the other = desync. Three variants:
- CL.TE — proxy uses CL, origin uses TE.
- TE.CL — proxy uses TE, origin uses CL.
- TE.TE — both speak TE but disagree on one slightly malformed.
Modern variant 1 — HTTP/2 downgrade
The classic next-gen vector. CDN speaks HTTP/2 to clients; sends HTTP/1.1 to origin. CDN re-serialises the request — and bug shapes appear in the re-serialisation.
1
2
Client → CDN: HTTP/2 frame { :method=GET, :path=/, ... }
CDN → Origin: GET / HTTP/1.1\r\nHost: ...\r\n...
If CDN copies attacker-controlled header values into the HTTP/1.1 line, CR/LF injection in the value smuggles a second request to origin:
1
2
3
Content-Length: 0
Transfer-Encoding: chunked
Host: example.com\r\nSmuggled-Header: x
becomes when re-serialised:
1
2
3
...
Host: example.com
Smuggled-Header: x
Splitting via CRLF in HTTP/2 value (forbidden by spec but accepted by some CDNs).
Modern variant 2 — HTTP/2 :path or :scheme injection
Pseudo-headers in HTTP/2 (:method, :path, :authority, :scheme) are normalised by some intermediaries but not others.
1
:path = /api/users\r\nX-Admin: yes
CDN treats as one URL; origin (after downgrade) sees two headers.
Modern variant 3 — header value normalisation gaps
Whitespace and case folding differ across proxies. A header like:
1
2
Transfer-Encoding: chunked
Transfer-encoding: identity
(Unicode invisible character before “identity”). Some proxies treat as one header (one wins); others as two.
Modern variant 4 — client-side desync (“Browser-Powered Desync”)
Browser sends a sequence of requests on a connection it keep-alives. If you can inject content into the response body that the browser misinterprets as the start of a new response, subsequent requests get poisoned with that misinterpreted boundary.
Example: a same-origin request returns:
1
2
3
4
HTTP/1.1 200 OK
Content-Length: 7
ABCDEFG
But what if the response body contains:
1
2
3
ABCDEFG
HTTP/1.1 200 OK
Content-Length: 50
The browser closes the response after 7 bytes; the trailing content waits on the socket. Next request: the in-flight bytes get prepended to the next response — that next response is now the attacker’s chosen content.
Real bugs require precise framing — but James Kettle’s research demonstrates it works against major sites.
Modern variant 5 — HTTP/3 frame smuggling
HTTP/3 frames inside QUIC streams. Smuggling primitives:
- QPACK dynamic table reuse across requests.
- Frame ordering differences between CDN and origin.
- Stream multiplexing — one stream’s framing affects another in some implementations.
Research is younger; expect more findings in 2026-2027.
Modern variant 6 — header / body mismatch via WebSocket upgrade
Sending a WebSocket upgrade request that the CDN forwards but origin rejects (or vice versa) can leave a half-open state where subsequent requests are smuggled into the upgrade body.
Modern variant 7 — extended CONNECT method
CONNECT in HTTP/2 (extended via :protocol for WebSocket-over-h2) creates tunnel semantics. CDN treats as tunnel-passthrough; origin treats as proxy request. Headers and body interpretation diverge.
Detection tools
- Burp Suite — HTTP Request Smuggler — automated probes (CL.TE, TE.CL, h2->h1 downgrade).
- smuggler.py (defparam) — CLI tool.
- Manual: send specific test frames; compare timing of front-end vs back-end response.
Tests every modern smuggling assessment includes
- Basic CL.TE and TE.CL probes.
- h2 → h1 downgrade with CR-LF in value.
- h2 :path injection.
- Header case folding fuzz.
- Header dup (
Content-Length: 5\r\nContent-Length: 7). - Pseudo-header injection (
:authoritywith newline). - Cache poisoning via smuggled request.
Impact
- Cache poisoning of arbitrary URLs.
- Credential capture by smuggling a request that the next victim’s session gets responded to.
- WAF bypass (origin sees a request the WAF didn’t filter).
- Internal endpoint reach (smuggled request bypasses front-end auth).
Defence
- One HTTP version end-to-end (h2 to origin if h2 to client).
- Reject ambiguous framing —
Content-LengthANDTransfer-Encodingtogether → 400. - Don’t copy headers verbatim during h2→h1 downgrade; re-canonicalise.
- Connection: close between front-end and back-end when uncertain.
- WAF that parses with the same library as origin.
- Continuous probing — run smuggler.py in production CI to detect regressions.
OSCP/OSEP/OSWE relevance
OSWE: an HTTP smuggling chain can be the “auth bypass” stage in a source-audited app. Bug bounty: smuggling found in major SaaS pays well — PortSwigger’s annual research updates.