Request Smuggling in HTTP/2 Downgrades
HTTP/2 is generally considered immune to classic request-smuggling because the length of each DATA frame is explicit. That protection disappears as soon as a front-end proxy “downgrades” the request to HTTP/1.x before forwarding it to a back-end. The moment two different parsers (the HTTP/2 front-end and the HTTP/1 back-end) try to agree on where one request ends and the next begins, all the old desync tricks come back – plus a few HTTP/2-only injection gadgets.[1]
Recent desync research reached the same conclusion from the defensive side: HTTP/2 at the edge does not save you if the proxy still speaks HTTP/1.1 upstream. The downgrade boundary is the attack surface.[2]
Why downgrades happen
- Browsers already speak HTTP/2, but much legacy origin infrastructure still only understands HTTP/1.1.
- Reverse-proxies (CDNs, WAFs, load-balancers) therefore terminate TLS + HTTP/2 at the edge and rewrite every request as HTTP/1.1 for the origin.
- That translation layer has to emit a single, valid HTTP/1.1 body delimiter for the origin.
- RFC 9113 is stricter than many downgrade implementations:
- connection-specific headers such as
transfer-encoding,upgrade,keep-alive, orproxy-connectionmake the HTTP/2 message malformed; teis only valid aste: trailers;- if
content-lengthis present, it must match the sum of the DATA frame payload lengths.
- connection-specific headers such as
Whenever the front-end trusts the HTTP/2 frame length but the back-end trusts CL or TE, an attacker can force them to disagree.
Two dominant primitive classes
| Variant | Front-end length | Back-end length | Typical payload |
|---|---|---|---|
| H2.TE | HTTP/2 frame | Transfer-Encoding: chunked | Embed an extra chunked message body whose final 0\r\n\r\n is not sent, so the back-end waits for the attacker-supplied “next” request. |
| H2.CL | HTTP/2 frame | Content-Length | Send a smaller CL than the real body, or inject content-length: 0 during downgrade, so the back-end reads past the boundary into the following request. |
These are identical in spirit to classic TE.CL / CL.TE, just with HTTP/2 replacing one of the parsers.
Identifying a downgrade chain
- Use ALPN in a TLS handshake (
openssl s_client -alpn h2 -connect host:443) or curl:
Ifcurl -v --http2 https://target* Using HTTP2appears, the edge speaks H2. - If the site looks HTTP/1.1-only, probe for hidden HTTP/2 anyway:
Some stacks support H2 but forget to advertise it via ALPN, which means you would otherwise miss downgrade-only bugs.curl --http2-prior-knowledge https://target - Send a deliberately malformed CL/TE request over HTTP/2 (Burp Repeater can force HTTP/2). If the response is an HTTP/1.1-style error such as
400 Bad chunkor a back-end-specific parse failure, you have strong evidence that the edge converted the traffic for an HTTP/1 parser downstream.
Exploitation workflow (H2.TE example)
:method: POST
:path: /login
:scheme: https
:authority: example.com
content-length: 13 # ignored by the edge
transfer-encoding: chunked
5;ext=1\r\nHELLO\r\n
0\r\n\r\nGET /admin HTTP/1.1\r\nHost: internal\r\nX: X
- The front-end reads exactly 13 bytes (
HELLO\r\n0\r\n\r\nGE), thinks the request is finished and forwards that much to the origin. - The back-end trusts the TE header, keeps reading until it sees the second
0\r\n\r\n, thereby consuming the prefix of the attacker’s second request (GET /admin …). - The remainder (
GET /admin …) is treated as a new request queued behind the victim’s.
Replace the smuggled request with:
POST /api/logoutto force session fixationGET /users/1234to steal a victim-specific resource
If you can only poison requests on your own client-mapped upstream connection, the bug can still be useful for cache poisoning, internal-header leaks, or front-end control bypass. For those reuse-locked cases, pivot to HTTP Connection Request Smuggling.
Modern downgrade-only injection gadgets
Many modern front-ends already strip a literal transfer-encoding: chunked header. Recent findings often work by manufacturing dangerous HTTP/1.1 bytes during the downgrade itself rather than sending them as a normal header.[1]
CRLF / LF injection in header values
HTTP/2 field values are binary. If the downgrade code fails to sanitize \r\n — or even a bare \n — you can often synthesize a backend-only header:
:method: POST
:path: /
:authority: example.com
foo: bar\r\ntransfer-encoding: chunked
0
GET /admin HTTP/1.1
Host: example.com
The edge sees a legal-ish HTTP/2 header block; the back-end receives a fresh Transfer-Encoding header and starts parsing the body as chunked. The same trick can be used to inject content-length: 0, terminate the header section early, or split one downgraded request into two.
Header-name injection
Some vendors fixed newline injection in values but forgot to validate names. If the downgrade code accepts spaces, colons, or other non-HTTP/1.1-safe bytes in a header name, the generated HTTP/1.1 request can contain multiple backend-visible header lines. This is a common way to re-introduce transfer-encoding after a partial hotfix.
Pseudo-header / request-line injection
A buggy mapper that copies :method, :path, :authority, or :scheme into the HTTP/1.1 request line without strict validation can let you:
- inject a full secondary request line,
- supply an ambiguous host or path,
- prepend a URL prefix that changes routing, cache keys, or SSRF targets.
If a literal TE header is blocked but pseudo-headers are not normalized before downgrade, this is the next place to look.
Related but distinct: h2c smuggling (clear-text upgrades)
This is not a CL/TE downgrade bug, but during the same assessment you should also test whether the edge forwards the HTTP/1.1 Upgrade: h2c header to a back-end that supports clear-text HTTP/2. If it does, you may be able to tunnel raw HTTP/2 frames through an edge that only validated the initial HTTP/1.1 request.
Key requirements:
- Edge forwards both
Connection: UpgradeandUpgrade: h2cunchanged. - Origin upgrades to HTTP/2 and keeps the connection-reuse semantics that enable request queueing or direct internal access.
For proxy-specific quirks and tunnel-focused payloads, see Upgrade Header Smuggling.
Notable real-world examples
- 2025 desync research – large shared edge providers were still exploitable because the dangerous trust boundary remained upstream HTTP/1.1, not the client-facing HTTP/2 session.[2]
- CVE-2023-25690 – Apache HTTP Server
mod_proxyrewrite rules could be chained into request splitting and smuggling when rewritten bytes were forwarded downstream. (fixed in 2.4.56) - CVE-2023-25950 – HAProxy 2.7.0 and 2.6.1-2.6.7 had an HTTP request/response smuggling issue in HTX handling that could alter a legitimate user’s request.
- CVE-2022-41721 – Go
MaxBytesHandlerleft unread body bytes that could later be interpreted as HTTP/2 frames, showing how “leftover bytes become a new protocol message” is not limited to classic H1 desync.
Tooling
- Burp Request Smuggler – since v3.0 (2025) it includes parser-discrepancy detection plus HTTP/2 tunnelling / header-injection probes. Enable HTTP/2 probing and ALPN override when hunting hidden H2 support.
- http2smugl – purpose-built H2→H1 detector and raw requester. The
requestsubcommand accepts escaped bytes such as\r,\n, and\x3a, which is handy when normal clients refuse malformed headers:go install github.com/neex/http2smugl@latest http2smugl detect https://target - SmuggleFuzz – fast downgrade scanner with customizable gadget lists and an explicit confirm mode:
go install github.com/moopinger/smugglefuzz@latest smugglefuzz scan -u https://target --confirm - h2cSmuggler – Python PoC by Bishop Fox to automate the clear-text upgrade attack:
python3 h2csmuggler.py -u https://target -x 'GET /admin HTTP/1.1\r\nHost: target\r\n\r\n' - curl / hyper – useful for quick ALPN checks and for replaying handcrafted HTTP/2 payloads:
curl --http2-prior-knowledge -X POST --data-binary @payload.raw https://target
Defensive measures
- Use upstream HTTP/2 end-to-end whenever possible – removing the H2→H1 translation step is the cleanest fix.
- Enforce RFC 9113 on ingress – reject HTTP/2 requests carrying connection-specific headers; only allow
te: trailers; reject mismatchedcontent-lengthvalues. - Strip and regenerate body-length metadata on downgrade – never forward attacker-supplied
Content-Length/Transfer-Encodinginto the downgraded request. - Normalize before mapping to HTTP/1.1 – reject or canonicalize CR, LF, colon, obs-fold, and non-ASCII bytes in header names, header values, and pseudo-headers before routing / rewrite logic.
- Reduce or isolate upstream connection reuse – if you are stuck on upstream HTTP/1.1, limiting shared back-end connections sharply reduces queue-poisoning impact.
- Strip
Upgradeunless it is explicitly required for WebSocket – preventsh2ctunnelling.