Request Tunnelling Attack Explained

Request Tunnelling Attack Explained

Written by

in

A request tunnelling attack uses a desync for a different goal than classic smuggling. Instead of poisoning a stranger’s request, the attacker opens a straight pipe through the front end to the back end and sends their own second request down it, where front end routing, access rules, and path restrictions never get a chance to look at it. The front end approved one request and, without realizing it, handed over a channel the attacker can keep talking on.

This builds on the same boundary confusion covered in our primer on http request smuggling, so read that first if the idea of two servers slicing one byte stream differently is new. Tunnelling is the self contained cousin of smuggling, and the difference is worth stating up front.

What a request tunnelling attack is, and what it is not

Classic smuggling needs a victim. The attacker leaves a prefix in the back end buffer and someone else’s request completes the exploit. That is both its strength, since the attacker can steal from or poison another user, and its weakness, since the attacker has to win a timing race against real traffic and cannot fully control what lands.

Tunnelling drops the victim entirely. The second request reaches the back end directly, on the same connection, as the attacker’s own follow on bytes. Nobody else is involved. That makes tunnelling less useful for stealing another user’s data and more useful for one specific job: reaching the back end as if the front end were not there. If the front end is where access control, path filtering, or header stripping lives, a tunnelled request walks in behind all of it.

Connection reuse is the thing being abused

To see why a tunnel is even possible, you have to be precise about connection reuse. A front end does not open a fresh connection to the back end for every incoming request. That would be slow. Instead it keeps a pool of open connections and sends many requests, from many users, down each one, one after another. The back end reads that connection as a continuous stream and parses request after request out of it.

The front end, in the normal case, parses the same stream and inspects each request as it goes by, applying routing and rules to every one. A tunnel exists when the attacker can make the front end stop parsing after the first request while the connection stays open to the back end. Everything the attacker sends after that point is no longer inspected as a request. It is just bytes flowing through an open pipe to a server that will happily parse them into requests.

Smuggling hides a request inside a request so another user runs it. Tunnelling stops the front end from reading, then speaks to the back end directly on a channel the front end is still holding open.

Two ways the pipe gets opened

Abusing a response the front end stops reading

Some front ends read exactly as many body bytes from the back end as they expect, based on the response headers, and then stop. If the attacker can cause the back end to emit a response the front end under reads, the leftover bytes sit on the connection. The attacker’s next request is then parsed by the back end, not the front end, because the front end already moved on. The connection has quietly become a tunnel: inspected on the first request, raw on everything after.

Abusing an upgrade

The cleaner version abuses a protocol upgrade. A client can ask to switch a connection to another protocol with an Upgrade header, for example to a WebSocket or to cleartext HTTP/2 with Upgrade: h2c. If the front end grants the upgrade and then blindly relays the raw connection to the back end, believing it is now carrying an upgraded protocol it no longer needs to inspect, the attacker simply keeps sending ordinary HTTP/1.1 requests. The back end reads them as normal requests, and the front end treats the whole exchange as opaque upgraded traffic. The attacker is now speaking straight to the back end.

The common thread is that the front end makes a decision, stop reading or hand off this connection, and then stops applying its rules while the back end keeps parsing. That gap is the tunnel.

What an attacker reaches through the tunnel

Because the back end trusts requests on an established connection, a tunnel lets the attacker send the requests the front end was built to refuse:

  • Restricted paths. Routes like an internal admin panel or a metadata endpoint that the front end blocks from the outside become reachable, since the tunnelled request never passes the block.
  • Stripped or spoofed headers. Headers the front end adds for trust, or strips for safety, can be set by the attacker directly, because the front end is not rewriting the tunnelled request.
  • Internal only virtual hosts. A Host value the front end would never route to can be sent straight to a back end that still honors it.

This makes tunnelling a close relative of using a desync to get around a filter, which we cover in request smuggling WAF bypass, and it overlaps with the trust assumptions behind connection state request smuggling, where a front end reuses a decision made on one request for the rest of a connection. All three come from the same root: the front end and the back end do not share one consistent view of what is happening on the connection between them. That root cause runs through our injection and input category.

Closing the tunnel

The defenses follow from the mechanism. The front end should not relay a raw connection to the back end after an upgrade unless it is certain the back end actually agreed to that upgrade, and it should refuse upgrade requests for protocols it does not fully broker. It should keep parsing and applying its rules to every request on a connection, not just the first. And where an upgrade or a surprising response could leave the connection in an uncertain state, closing that connection rather than returning it to the pool removes the pipe before anyone can speak through it.

A tunnel is a subtle bug because every individual request in it is well formed and the front end believes it is behaving correctly. Confirming one takes the same care as any desync: you prove that a request actually reached the back end unfiltered, with a safe and specific piece of evidence, rather than guessing from a header that looked odd. That style of work, reasoning about what each server assumes about the one next to it and then proving the assumption false, is what UnboundCompute is built around, and you can read more on our about page.

Frequently asked questions

What is a request tunnelling attack?

It is a use of a desync where the attacker opens a straight channel through the front end to the back end and sends their own second request down it. The front end stops inspecting the connection after the first request, so the tunnelled request reaches the back end without passing routing, access rules, or path restrictions. It reaches the back end directly rather than poisoning another user.

How is tunnelling different from classic request smuggling?

Classic smuggling leaves a prefix in the back end buffer that completes when some other user sends a request, so it needs a victim and a timing race. Tunnelling drops the victim entirely: the attacker’s own follow on bytes reach the back end on the same connection. That makes it weaker for stealing another user’s data but strong for reaching the back end as if the front end were not there.

How does connection reuse make tunnelling possible?

A front end keeps a pool of open connections to the back end and sends many requests from many users down each one. The back end parses that stream request after request. A tunnel exists when the attacker makes the front end stop parsing after the first request while the connection stays open, often by abusing an Upgrade header or a response the front end under reads, so later bytes are parsed only by the back end.

How do you defend against request tunnelling?

The front end should not relay a raw connection to the back end after an upgrade unless the back end truly agreed to it, and should refuse upgrades for protocols it does not fully broker. It should keep applying its rules to every request on a connection, not just the first. Where an upgrade or an odd response leaves the connection uncertain, closing it instead of returning it to the pool removes the pipe.


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.