Most smuggling bugs come from two servers disagreeing about where a request ends. h2c smuggling is different: it comes from a front end that blindly forwards an upgrade handshake, letting a client open a cleartext HTTP/2 tunnel straight through to the back end. Once that tunnel exists, the client talks to the back end directly and the front end stops inspecting anything, which makes it a quiet cousin of ordinary http request smuggling that bypasses routing and controls instead of poisoning a queue.
What h2c is and how the upgrade works
h2c is cleartext HTTP/2, meaning HTTP/2 spoken over a plain TCP connection with no TLS. A client that wants to move a normal HTTP/1.1 connection up to h2c asks for it with an upgrade handshake. It sends a request with two headers that work together:
GET / HTTP/1.1 Host: acme-notes.example Upgrade: h2c Connection: Upgrade, HTTP2-Settings HTTP2-Settings: AAMAAABkAAQAAP__
The Upgrade header names the protocol the client wants to switch to. The Connection: Upgrade header marks the upgrade as a hop by hop instruction, meaning it is addressed to the next server in the chain and should not be passed further. If a server agrees, it replies with a 101 Switching Protocols status and from that point the connection is no longer HTTP/1.1; it is a raw h2c connection carrying HTTP/2 frames.
Why h2c smuggling works
The Connection and Upgrade headers are hop by hop by design. A front end that handles its own connections correctly should consume the upgrade request, decide for itself whether to switch protocols, and never forward those headers to the back end. The bug appears when the front end treats the upgrade request as an ordinary request to proxy and passes the Upgrade and Connection headers straight through.
When that happens, the back end receives the handshake, agrees to it, and returns 101 Switching Protocols. The front end relays that response to the client and, because it thinks it is just proxying an ordinary exchange, it keeps the underlying connection open as a dumb pipe. The client now has an h2c connection that runs end to end to the back end, with the front end relaying bytes it no longer parses.
The front end did not get tricked into misreading a boundary. It got tricked into stepping out of the way entirely.
What the tunnel lets an attacker do
The reason this matters is that the front end is usually where the security controls live. It enforces path rules, strips dangerous headers, applies rate limits, and decides which routes the public is allowed to reach. All of that runs on HTTP/1.1 requests the front end parses. Once the client has an h2c tunnel, every request it sends inside that tunnel is framed HTTP/2 that the front end is only relaying, so none of those controls apply.
- Reaching blocked routes. A front end that refuses
/adminor an internal metrics path checks the HTTP/1.1 request line. Inside the tunnel the attacker sends an HTTP/2 request straight to the back end for exactly those paths, and the front end never sees a request line to block. - Undoing header rules. Headers the front end would normally add, strip, or rewrite, such as an authentication header it injects or a client address header it overwrites, are skipped, because the front end is no longer building the request the back end reads.
- Escaping rate and access limits. Controls keyed on parsed requests stop counting once the traffic is opaque frames the front end just forwards.
The effect is close to reaching the back end as if the front end were not there. That is why h2c smuggling is sometimes described as request tunnelling rather than a desync, and our note on the broader request tunnelling attack covers the same shape of problem from the tunnelling side.
A concrete walk through
Picture Acme Notes behind a front end that routes by path and blocks anything under /internal/. The attacker first sends the upgrade request shown above against a path the front end does allow, so the request passes the route check. The front end, not realizing the Upgrade and Connection headers are meant for it, forwards them to the back end. The back end supports h2c, answers 101 Switching Protocols, and the tunnel is open.
Now the attacker sends a second request inside the tunnel as an HTTP/2 frame, this time for /internal/config. The front end is relaying raw bytes and never parses that path, so its block rule never fires. The back end receives a perfectly valid HTTP/2 request and serves the internal page. The attacker reached a route the front end exists to protect, without ever smuggling a byte past a parser, because the parser was switched off by the handshake.
How to shut it
The fix is to treat the upgrade headers as what they are. A front end should strip Connection, Upgrade, and the HTTP2-Settings header from requests before forwarding them, so a back end never sees an upgrade the client was not allowed to negotiate. If the front end does not itself support switching a connection to h2c, it should refuse the request rather than proxy the handshake. Back ends that do not need to accept h2c from a proxy should have it turned off, so a forwarded handshake has nothing to agree to. These are all ways of saying the same thing: the server that owns the controls must also own the decision to change protocols, and this family of input handling bugs is the kind we group under injection and input.
What makes h2c smuggling easy to miss is that nothing in the request is malformed. The handshake is legal, the paths are legal, and the only mistake is a forwarding decision about which headers are hop by hop. UnboundCompute reasons about that reachability rather than matching header patterns, asking whether an upgrade a client sends can actually reach a back end that will accept it and open a path around the controls. You can read more about that approach on our about page. The request never breaks the rules. It just arranges to be read by a server that was never told the rules.
Frequently asked questions
What is h2c smuggling?
h2c is cleartext HTTP/2, meaning HTTP/2 over a plain TCP connection with no TLS. h2c smuggling happens when a front end blindly forwards an upgrade handshake to the back end instead of handling it itself. The back end agrees, a cleartext HTTP/2 tunnel opens end to end, and the front end relays raw frames it no longer parses, so its routing and security controls stop applying.
How does the upgrade let a client build a tunnel?
A client sends a request with Upgrade: h2c and Connection: Upgrade. Those headers are hop by hop, meaning they are addressed to the next server and should not be forwarded. A front end that proxies them anyway lets the back end reply 101 Switching Protocols. From then on the connection is raw h2c and the client talks straight to the back end through the front end, which has become a dumb pipe.
What can an attacker do through the tunnel?
Because the front end usually holds the controls, a tunnel lets the client skip them. The attacker can reach routes the front end blocks such as an admin or internal path, undo headers the front end would add or strip, and escape rate and access limits that only count parsed requests. The effect is close to reaching the back end as if the front end were not there, which is why it is a form of request tunnelling.
How do you prevent h2c smuggling?
Strip Connection, Upgrade, and HTTP2-Settings from requests before forwarding, so the back end never sees an upgrade the client was not allowed to negotiate. If the front end does not itself support switching to h2c, it should refuse the request rather than proxy the handshake, and back ends that do not need h2c from a proxy should have it turned off.
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.
