October 20, 2026
$16,500 for a Protocol Downgrade Nobody Double-Checked: HTTP/2 Desync Smuggling
Iβd found request smuggling before, built on the classic Content-Length versus Transfer-Encoding disagreement between a front-end proxy andβ¦

By T4nv1
5 min read
I'd found request smuggling before, built on the classic Content-Length versus Transfer-Encoding disagreement between a front-end proxy and a back-end server. This bug lives in the same general neighborhood β two systems disagreeing about where one request ends and the next begins β but the mechanism is different enough that none of my old detection habits transferred directly. The disagreement here wasn't about conflicting headers within one protocol. It was about what happens when a connection negotiated as HTTP/2 gets silently translated into HTTP/1.1 somewhere inside the request's path, and that translation step doesn't get revalidated with the same rigor the original protocol enforced automatically.
The target, which I'll call Granite Hollow here per their program's disclosure terms, runs a modern edge architecture β clients connect over HTTP/2, which the edge layer then downgrades to HTTP/1.1 before forwarding to a legacy application tier that was never updated to speak HTTP/2 natively. This downgrade pattern is extremely common in production infrastructure, precisely because it lets an organization modernize client-facing performance without needing to touch every internal service at once.
Why HTTP/2 Is Supposed to Make This Entire Bug Class Impossible
Classic HTTP/1.1 request smuggling exists because HTTP/1.1 allows two different ways of indicating how long a request body is β Content-Length and Transfer-Encoding: chunked β and the specification's rule for handling both headers present at once is something real-world parsers implement inconsistently. HTTP/2 was deliberately designed to eliminate this entire category of ambiguity: it uses binary framing with explicit length fields for every piece of a message, with no equivalent to chunked encoding's text-based terminator sequences and no scenario where two conflicting length indicators can coexist in a single, well-formed HTTP/2 request. Request smuggling against a pure HTTP/2 connection, end to end, is substantially harder to construct for exactly this reason.
The catch is that very few backend systems actually run HTTP/2 end to end. The moment an HTTP/2 request gets downgraded into HTTP/1.1 to reach a legacy backend, the conversion process has to reconstruct Content-Length or Transfer-Encoding headers from HTTP/2's binary length fields β and that reconstruction step is where an old vulnerability class quietly becomes possible again, inside infrastructure whose operators may have reasonably assumed HTTP/2 made it a non-issue.
Looking for the Downgrade Point
I confirmed Granite Hollow's edge layer negotiated HTTP/2 with clients through a straightforward protocol inspection, then went looking for behavioral evidence that something behind it was still speaking HTTP/1.1 β inconsistent handling of HTTP/1.1-specific headers passed through, response timing patterns suggesting a translation layer, and ultimately a direct tell: a deliberately malformed Transfer-Encoding header value, sent as an HTTP/2 header field, which should be meaningless in a protocol with no concept of chunked transfer encoding at all, produced a distinctly different backend response than a clean request did. That's not supposed to happen if a request stays HTTP/2 the entire way through β the only way a header named Transfer-Encoding could meaningfully affect backend processing at all is if something downstream was reconstructing an HTTP/1.1 request and actually interpreting it.
Constructing the Smuggling Payload Through an HTTP/2 Request
The core technique required sending an HTTP/2 request whose headers, once translated into HTTP/1.1 by the downgrade layer, would produce the same kind of Content-Length/Transfer-Encoding disagreement classic smuggling depends on β except now the ambiguity had to be smuggled in through HTTP/2's header fields rather than constructed directly as raw HTTP/1.1 text.
I sent an HTTP/2 request including both a content-length header and a transfer-encoding: chunked header as explicit HTTP/2 header fields:
:method: POST
:path: /login
content-length: 4
transfer-encoding: chunked
1
Z
Q:method: POST
:path: /login
content-length: 4
transfer-encoding: chunked
1
Z
QTechnically, a well-behaved HTTP/2 implementation should strip or reject transfer-encoding as a header field entirely, since it has no meaning in HTTP/2's binary framing β but the behavior of the downgrade layer itself, not the HTTP/2 parsing layer in front of it, was the actual question. If the downgrade process naively reconstructed an HTTP/1.1 request by carrying both headers through unmodified into the translated request, the exact same Content-Length-versus-Transfer-Encoding disagreement I'd exploited in prior, pure-HTTP/1.1 smuggling would reappear β just now triggered by something an HTTP/2 client was never supposed to be able to influence at all.
Timing confirmed it. The downgraded backend hung, waiting for chunked-body termination bytes that the front-end's content-length-based forwarding had already cut off β the same tell-tale delay signature from classic CL.TE smuggling, now reproduced through an HTTP/2 connection that, by specification, shouldn't have carried this ambiguity through at all.
Building the Full Exploit
With the desync primitive confirmed, I constructed a payload designed to smuggle a complete, separate request onto the front of whatever the next request on the same downgraded backend connection turned out to be β structurally similar to classic HTTP/1.1 smuggling, but now assembled as HTTP/2 header fields and a request body that would reconstruct correctly once downgraded:
:method: POST
:path: /login
content-length: 130
transfer-encoding: chunked
0
GET /account/settings HTTP/1.1
Host: granite-hollow-target.com
X-Ignore: X:method: POST
:path: /login
content-length: 130
transfer-encoding: chunked
0
GET /account/settings HTTP/1.1
Host: granite-hollow-target.com
X-Ignore: XFollowing the translation layer's content-length: 130 for forwarding purposes while the backend, now reconstructed as HTTP/1.1 and reading via chunked-encoding rules, interpreted the 0\r\n\r\n as the end of the body β leaving my embedded GET /account/settings request sitting in the backend's buffer, ready to be prepended onto whatever request arrived next on that same reused downgraded connection, exactly as in classic request smuggling, just arrived at through a protocol downgrade rather than raw HTTP/1.1 header manipulation.
Confirming Response Queue Poisoning, Self-Contained
As with any smuggling proof of concept, demonstrating real impact responsibly meant never touching a real user's traffic. I ran the full confirmation using two browser sessions of my own, carefully timed so that my own second session, not a stranger's, was the one whose request landed on the poisoned connection β confirming that Session B received response content intended for my smuggled Session A request, demonstrating the complete response-queue poisoning mechanism without any ambiguity about a real victim being involved.
The Report
Title: HTTP/2-to-HTTP/1.1 downgrade desync allows request smuggling via Content-Length/Transfer-Encoding header reconstruction ambiguity
Root cause: The edge layer negotiates HTTP/2 with clients but downgrades requests to HTTP/1.1 before forwarding to a legacy backend. The downgrade process reconstructs HTTP/1.1 headers from HTTP/2 header fields without stripping or rejecting transfer-encoding, which has no valid meaning in HTTP/2, allowing a client to smuggle the classic Content-Length/Transfer-Encoding ambiguity through an HTTP/2 connection into the reconstructed HTTP/1.1 request, reintroducing a request-smuggling primitive that pure HTTP/2 is specifically designed to prevent.
Reproduction: The malformed-header behavioral tell, the timing-based CL.TE confirmation reproduced through HTTP/2, and the full two-session response-queue-poisoning demonstration, performed entirely between two sessions under my own control.
Impact: Any attacker able to send HTTP/2 requests to the edge layer can poison the shared downgraded connection's response queue, potentially exposing other users' session data or account contents depending on what the next request on the affected connection happens to be.
Fix recommendations:
- Strip
transfer-encodingand any other HTTP/1.1-specific header field entirely during HTTP/2-to-HTTP/1.1 downgrade, since it carries no valid meaning in HTTP/2 and should never be allowed to influence the reconstructed request - Disable connection reuse between the downgrade layer and the legacy backend, so a poisoned connection can't carry state across unrelated users' requests
- Treat the downgrade boundary as its own distinct attack surface during security review, separate from both the HTTP/2 edge layer and the HTTP/1.1 backend individually, since this vulnerability class only exists in the translation between them
What Happened After
Granite Hollow's infrastructure team moved urgently once response-queue poisoning was confirmed. They patched the downgrade layer to strip the offending header within four days and began evaluating a longer-term move toward native HTTP/2 support on the backend tier to eliminate the downgrade boundary entirely. The report closed at $16,500, rated critical, reflecting both the severity of cross-user data exposure and the fact that this reintroduced a vulnerability class the organization likely believed their HTTP/2 migration had already closed off.
Why This Variant Deserves Specific Attention in 2026
As more infrastructure adopts HTTP/2 and HTTP/3 at the edge while legacy backends lag behind, downgrade and translation boundaries are becoming one of the more consequential places request smuggling resurfaces β precisely because teams reasonably assume a newer protocol closes off an older protocol's well-known weaknesses, without auditing whether a translation layer quietly reopens them. Any architecture mixing protocol versions across its request path deserves this specific test, regardless of how modern the client-facing layer looks.