request-smuggling-in-http-2-downgrades.md (10547B)
1 --- 2 title: "Request Smuggling in HTTP/2 Downgrades" 3 section: "Web Pentesting" 4 sectionSlug: "pentesting-web" 5 sourcePath: "src/pentesting-web/http-request-smuggling/request-smuggling-in-http-2-downgrades.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/http-request-smuggling/request-smuggling-in-http-2-downgrades.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # Request Smuggling in HTTP/2 Downgrades 14 15 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.<sup>[[1]](#references)</sup> 16 17 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.<sup>[[2]](#references)</sup> 18 19 --- 20 ## Why downgrades happen 21 22 1. Browsers already speak HTTP/2, but much legacy origin infrastructure still only understands HTTP/1.1. 23 2. 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. 24 3. That translation layer has to emit a single, valid HTTP/1.1 body delimiter for the origin. 25 4. RFC 9113 is stricter than many downgrade implementations: 26 * connection-specific headers such as `transfer-encoding`, `upgrade`, `keep-alive`, or `proxy-connection` make the HTTP/2 message malformed; 27 * `te` is only valid as `te: trailers`; 28 * if `content-length` is present, it must match the sum of the DATA frame payload lengths. 29 30 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. 31 32 --- 33 ## Two dominant primitive classes 34 35 | Variant | Front-end length | Back-end length | Typical payload | 36 |---------|-----------------|-----------------|-----------------| 37 | **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. | 38 | **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. | 39 40 > These are identical in spirit to classic TE.CL / CL.TE, just with HTTP/2 replacing one of the parsers. 41 42 --- 43 ## Identifying a downgrade chain 44 45 1. Use **ALPN** in a TLS handshake (`openssl s_client -alpn h2 -connect host:443`) or **curl**: 46 ```bash 47 curl -v --http2 https://target 48 ``` 49 If `* Using HTTP2` appears, the edge speaks H2. 50 2. If the site *looks* HTTP/1.1-only, probe for **hidden HTTP/2** anyway: 51 ```bash 52 curl --http2-prior-knowledge https://target 53 ``` 54 Some stacks support H2 but forget to advertise it via ALPN, which means you would otherwise miss downgrade-only bugs. 55 3. 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 chunk` or a back-end-specific parse failure, you have strong evidence that the edge converted the traffic for an HTTP/1 parser downstream. 56 57 --- 58 ## Exploitation workflow (H2.TE example) 59 60 ```http 61 :method: POST 62 :path: /login 63 :scheme: https 64 :authority: example.com 65 content-length: 13 # ignored by the edge 66 transfer-encoding: chunked 67 68 5;ext=1\r\nHELLO\r\n 69 0\r\n\r\nGET /admin HTTP/1.1\r\nHost: internal\r\nX: X 70 ``` 71 1. 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. 72 2. 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 …`). 73 3. The remainder (`GET /admin …`) is treated as a *new* request queued behind the victim’s. 74 75 Replace the smuggled request with: 76 * `POST /api/logout` to force session fixation 77 * `GET /users/1234` to steal a victim-specific resource 78 79 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](/hacktricks/pentesting-web/http-connection-request-smuggling). 80 81 --- 82 ## Modern downgrade-only injection gadgets 83 84 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.<sup>[[1]](#references)</sup> 85 86 ### CRLF / LF injection in header values 87 88 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: 89 90 ```http 91 :method: POST 92 :path: / 93 :authority: example.com 94 foo: bar\r\ntransfer-encoding: chunked 95 96 0 97 98 GET /admin HTTP/1.1 99 Host: example.com 100 ``` 101 102 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. 103 104 ### Header-name injection 105 106 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. 107 108 ### Pseudo-header / request-line injection 109 110 A buggy mapper that copies `:method`, `:path`, `:authority`, or `:scheme` into the HTTP/1.1 request line without strict validation can let you: 111 112 * inject a full secondary request line, 113 * supply an ambiguous host or path, 114 * prepend a URL prefix that changes routing, cache keys, or SSRF targets. 115 116 If a literal `TE` header is blocked but pseudo-headers are not normalized before downgrade, this is the next place to look. 117 118 --- 119 ## Related but distinct: h2c smuggling (clear-text upgrades) 120 121 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. 122 123 Key requirements: 124 * Edge forwards **both** `Connection: Upgrade` and `Upgrade: h2c` unchanged. 125 * Origin upgrades to HTTP/2 and keeps the connection-reuse semantics that enable request queueing or direct internal access. 126 127 For proxy-specific quirks and tunnel-focused payloads, see [Upgrade Header Smuggling](/hacktricks/pentesting-web/h2c-smuggling). 128 129 --- 130 ## Notable real-world examples 131 132 * **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.<sup>[[2]](#references)</sup> 133 * **CVE-2023-25690** – Apache HTTP Server `mod_proxy` rewrite rules could be chained into request splitting and smuggling when rewritten bytes were forwarded downstream. (fixed in 2.4.56) 134 * **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. 135 * **CVE-2022-41721** – Go `MaxBytesHandler` left 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. 136 137 --- 138 ## Tooling 139 140 * **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. 141 * [**http2smugl**](https://github.com/neex/http2smugl) – purpose-built H2→H1 detector and raw requester. The `request` subcommand accepts escaped bytes such as `\r`, `\n`, and `\x3a`, which is handy when normal clients refuse malformed headers: 142 ```bash 143 go install github.com/neex/http2smugl@latest 144 http2smugl detect https://target 145 ``` 146 * [**SmuggleFuzz**](https://github.com/Moopinger/smugglefuzz) – fast downgrade scanner with customizable gadget lists and an explicit confirm mode: 147 ```bash 148 go install github.com/moopinger/smugglefuzz@latest 149 smugglefuzz scan -u https://target --confirm 150 ``` 151 * **h2cSmuggler** – Python PoC by Bishop Fox to automate the clear-text upgrade attack: 152 ```bash 153 python3 h2csmuggler.py -u https://target -x 'GET /admin HTTP/1.1\r\nHost: target\r\n\r\n' 154 ``` 155 * **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` 156 157 --- 158 ## Defensive measures 159 160 1. **Use upstream HTTP/2 end-to-end whenever possible** – removing the H2→H1 translation step is the cleanest fix. 161 2. **Enforce RFC 9113 on ingress** – reject HTTP/2 requests carrying connection-specific headers; only allow `te: trailers`; reject mismatched `content-length` values. 162 3. **Strip and regenerate body-length metadata on downgrade** – never forward attacker-supplied `Content-Length` / `Transfer-Encoding` into the downgraded request. 163 4. **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. 164 5. **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. 165 6. **Strip `Upgrade` unless it is explicitly required for WebSocket** – prevents `h2c` tunnelling. 166 167 --- 168 ## References 169 170 - [1] [PortSwigger Research - HTTP/2: The Sequel is Always Worse](https://portswigger.net/research/http2) 171 - [2] [PortSwigger Research - HTTP/1.1 must die: the desync endgame](https://portswigger.net/research/http1-must-die)