HTTP/2 Downgrade Smuggling Explained

HTTP/2 Downgrade Smuggling Explained

Written by

in

HTTP/2 was meant to end request smuggling for good, and inside its own framing it does. HTTP/2 downgrade smuggling is what happens when a front end speaks HTTP/2 to the browser but rewrites each request as HTTP/1.1 before handing it to a back end, and that rewrite quietly reintroduces the length ambiguity HTTP/2 had removed. It is one of the most common modern paths into http request smuggling, because it reopens the bug on chains that look modern from the outside.

Why HTTP/2 was supposed to fix smuggling

Classic smuggling comes from HTTP/1.1 having two ways to say how long a body is. Content-Length gives a byte count, and Transfer-Encoding: chunked sends sized chunks ending in a zero chunk. When a request carries both and they disagree, two servers can slice the stream at different bytes, and the leftover becomes a request the attacker wrote.

HTTP/2 does not send headers and bodies as a text stream you have to slice. Each message body travels in binary data frames, and the framing itself carries the length. There is one unambiguous answer to where a message ends, set by the frames, not by two text headers that have to agree. End to end over HTTP/2 there is simply no place for a length disagreement to hide. If the whole chain spoke HTTP/2 from the browser to the back end, this class of bug would mostly be finished.

Where HTTP/2 downgrade smuggling reopens it

The chain rarely speaks HTTP/2 the whole way. Most front ends accept HTTP/2 from the internet and then rewrite each request into HTTP/1.1 for a back end that still speaks the older protocol. That rewrite is the downgrade, and it is where HTTP/2 downgrade smuggling lives.

To downgrade, the front end has to invent the HTTP/1.1 length headers from the HTTP/2 frame data. It writes a Content-Length, or it copies across a length header the request carried. If it does this carelessly, the back end is once again reading length from a text header that may not match the real body, and the attacker has a boundary to forge.

HTTP/2 removed the ambiguity from the wire. The downgrade puts it back by hand, on the one hop you cannot see.

H2.CL, a forged content length

In HTTP/2 the true body length is fixed by the data frames, so the content-length field a client sends is only a claim the server is supposed to check against those frames. An attacker sends a request whose declared content-length is smaller than the actual body carried in the frames.

:method POST
:path /
content-length: 4

GET /admin HTTP/1.1
Host: acme-notes.example
...

If the front end fails to validate that claim and trusts it during the downgrade, it writes Content-Length: 4 into the HTTP/1.1 request it forwards. The back end reads only four bytes as the body and treats the rest, the smuggled GET /admin lines, as the start of the next request on the connection. Same prefix, same poisoned queue, reached through a length the front end should have recomputed from the frames.

H2.TE, a transfer encoding that should not exist

The HTTP/2 standard says a request carrying a transfer-encoding header is malformed and must be rejected, because chunked encoding has no meaning inside HTTP/2 framing. A front end that forwards that header anyway hands the back end a Transfer-Encoding: chunked on a downgraded request. The back end honors it, reads the body as chunks regardless of the length the front end wrote, and desyncs. The attacker reached a classic chunked smuggle through a header the front end should have thrown away at the door.

Splitting a request through injected newlines

Length is not the only way in. HTTP/2 keeps header names and values in a binary format where a newline is just data, so a value is allowed to contain bytes that would be structural in HTTP/1.1. When the front end flattens that value into a single text line during the downgrade, any carriage return and line feed the attacker hid inside it gets written literally into the HTTP/1.1 stream. That lets the attacker end one header early and start new lines the back end reads as a second request. This splitting path is a close relative of plain HTTP/2 CRLF header injection; the difference is whether the injected bytes forge a whole new request boundary or just inject one extra header.

Why the downgrade case matters so much

Pure HTTP/1.1 smuggling needs two HTTP/1.1 servers that parse length differently, and careful operators had been closing those gaps for years. The downgrade widened the target again. It reopened the bug on deployments that look safe from the outside, where the public facing server speaks HTTP/2 and only the internal hop still speaks HTTP/1.1. The front end that was supposed to be the fix became the place the request gets rewritten into an ambiguous form.

The rewrite also carries more than length. Header names, header values, and the HTTP/2 pseudo headers for method and path all have to collapse into one text request line and a block of HTTP/1.1 headers. Anywhere a special character survives that collapse, a new request boundary can be forged, which is why downgrade smuggling and request splitting keep showing up together. Our walk through CL.TE request smuggling covers the length half of this in the HTTP/1.1 setting the downgrade lands in, and the whole family sits under injection and input.

Closing the gap

The defenses are about the rewrite, not about blocking payloads. Validate the content-length a client sends against the real frame data and recompute it rather than trusting it. Drop any transfer-encoding the HTTP/2 standard calls malformed instead of forwarding it. Refuse header names and values that carry control characters so nothing survives the flatten into a forged boundary. Best of all, speak HTTP/2 to the back end too, because a chain with no downgrade has no place to forge a length header.

What makes this bug hard for a scanner is that every byte is legal HTTP/2 on its own; the flaw only appears after the rewrite, on a hop the scanner never sees. UnboundCompute reasons about that reachability rather than matching header patterns, following whether a client controlled value actually survives the downgrade and reaches a second parser that reads it as a new boundary. You can read more about how that works on our about page. HTTP/2 closed the door. The downgrade is someone propping it back open on the way to the back end.

Frequently asked questions

What is HTTP/2 downgrade smuggling?

It is request smuggling that appears when a front end speaks HTTP/2 to the client but rewrites each request as HTTP/1.1 for a back end. HTTP/2 carries body length unambiguously in its binary frames, but the rewrite has to invent HTTP/1.1 length headers, and if it does that carelessly the back end reads length from a text header that no longer matches the real body, which reopens the old desync.

Why was HTTP/2 supposed to fix smuggling?

Classic smuggling comes from HTTP/1.1 having two ways to declare body length, Content-Length and Transfer-Encoding: chunked, which can disagree. HTTP/2 sends each body in binary data frames whose length is set by the framing, not by text headers, so there is one unambiguous answer to where a message ends. End to end over HTTP/2 there is no place for a length disagreement to hide.

What are H2.CL and H2.TE?

They name the two downgrade desyncs. H2.CL is when the front end trusts a content-length the client sent instead of validating it against the frames, then writes that wrong length into the forwarded HTTP/1.1 request. H2.TE is when the front end forwards a transfer-encoding header the HTTP/2 standard calls malformed, and the back end honors it as chunked. Both leave a smuggled prefix in the back end buffer.

How do you stop downgrade smuggling?

Recompute the content-length from the real frame data rather than trusting the client claim, drop any transfer-encoding the HTTP/2 standard calls malformed, and refuse header names and values that carry control characters so nothing survives the flatten into a forged boundary. Best of all, speak HTTP/2 to the back end too, because a chain with no downgrade has no place to forge a length header.


Put an autonomous researcher on your own systems

UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.