Request smuggling: when the proxy and the server disagree
CL.TE, TE.CL, TE.TE and the HTTP/2 downgrade desyncs. How two servers reading the same bytes can disagree about where one request ends, what that buys you, and how to detect it without wrecking the target.
Almost every real deployment has at least two HTTP implementations in a line: a CDN or load balancer in front, an application server behind, sharing a persistent connection between them. Request smuggling exists because those two implementations can read the same bytes and disagree about where one request stops and the next begins.
When they disagree, part of your request is left in the connection's buffer and gets *prepended* to whatever request arrives next - which is somebody else's. That is the primitive: you control the beginning of another user's request.
Why they disagree: two ways to say how long a body is
HTTP/1.1 offers two mechanisms and the specification says you must not use both. So of course a request with both is where the trouble is.
Content-Length: 13 the body is exactly 13 bytes
Transfer-Encoding: chunked the body is a sequence of size-prefixed chunks,
ending with a zero-size chunk:
5\r\nhello\r\n0\r\n\r\n| Variant | Front-end uses | Back-end uses | Consequence |
|---|---|---|---|
| CL.TE | Content-Length | Transfer-Encoding | The front-end forwards more bytes than the back-end consumes. The remainder is a prefix. |
| TE.CL | Transfer-Encoding | Content-Length | The back-end stops early. The rest of the chunked body starts the next request. |
| TE.TE | Both, but one is obfuscated | The other | An unusual header spelling makes exactly one of them ignore it. |
| H2.CL / H2.TE | HTTP/2, downgraded to 1.1 | HTTP/1.1 | The front-end rewrites the request and trusts a length header the client controls. |
| CL.0 | Content-Length | Ignores the body entirely | Any body is treated as a new request. Common against endpoints that never expect one. |
TE.TE obfuscation
The header only has to be recognised by one of the two servers. Any of these will be honoured by some implementations and ignored by others:
Transfer-Encoding: xchunked
Transfer-Encoding : chunked (space before the colon)
Transfer-Encoding: chunked (leading tab in the value)
Transfer-Encoding
: chunked (obs-fold continuation)
X: X[\n]Transfer-Encoding: chunked (embedded newline)
Transfer-Encoding: chunked, identityDetecting it without breaking the target
The safe detection method is timing, and the reason it is safe is that it does not leave a poisoned prefix in a shared connection.
CL.TE, detected by timeout
POST / HTTP/1.1
Host: target
Content-Length: 4
Transfer-Encoding: chunked
1
A
X1\r\nA\r\n chunk, and then waits for a terminating chunk that never arrives - so the response hangs. A delay here that does not occur without the Transfer-Encoding header is your signal.TE.CL, detected the same way
POST / HTTP/1.1
Host: target
Content-Length: 6
Transfer-Encoding: chunked
0
XWhat it buys you
- Bypassing front-end access control. The proxy enforces
/adminrestrictions. Smuggle a request for/adminpast it and the back-end, which trusts the proxy, serves it. - Capturing another user's request. Smuggle a prefix that is a POST to an endpoint that reflects its body - a comment form, a search log - and the victim's request, including their
Cookieheader, becomes the body. This is session hijacking without any XSS. - Response queue poisoning. Desync the connection so responses shift by one. Every subsequent user on that connection receives the response to the previous user's request. This is the most severe form: it steals sessions continuously and needs no reflection.
- Web cache poisoning. Smuggle a request whose response gets cached under a key other users will request. Turns a single desync into a persistent, broadly-scoped XSS or redirect.
- Turning a reflected bug into a stored one. Any client-side bug that needed a victim to click a link becomes something the victim receives simply by browsing.
POST / HTTP/1.1
Host: target
Content-Length: 60
Transfer-Encoding: chunked
0
GET /admin HTTP/1.1
X-Ignore: X-Ignore: swallows the next victim's request line so the smuggled request stays well-formed.HTTP/2 downgrades, the modern variant
HTTP/2 has no ambiguity about body length - it is framed - so a pure HTTP/2 path cannot be smuggled. But most CDNs speak HTTP/2 to the client and HTTP/1.1 to the origin, which means they *rewrite* every request. If the rewrite copies an attacker-supplied content-length header, or fails to reject header values containing \r\n, the ambiguity is reintroduced by the translation itself.
- H2.CL: send an HTTP/2 request with an explicit
content-lengththat disagrees with the actual frame length. A downgrading proxy that trusts the header creates a CL desync at the origin. - H2.TE: include
transfer-encoding: chunkedin an HTTP/2 request. It is illegal, and a proxy that forwards it rather than rejecting it produces a TE desync. - CRLF injection in a header value or a pseudo-header. HTTP/2 header values are binary-safe, so
\r\nin a value is legal there and becomes a header separator after downgrade. This also lets you inject an entire second request into a single header. - Request tunnelling. Even when the connection is not reused across users, you can still smuggle a request whose response comes back to *you* - which reaches internal endpoints without affecting anyone else, and is the safest useful variant.
In a CTF, how to know it is this
Request smuggling is expensive to test, so it helps to recognise when a challenge is pointing at it:
- The architecture is visible and has two layers - a
docker-compose.ymlwith nginx or haproxy in front of a Node, Python or Go app. - There is an endpoint the front-end explicitly blocks, and the challenge text draws attention to it.
- There is an admin bot browsing the site, which is the victim whose request you need to capture. The same bot that makes XSS challenges work makes smuggling challenges work.
- The two components are known to disagree - a proxy written in one language and an app server in another, especially one with a hand-rolled HTTP parser.
- Response headers reveal a chain:
Via,X-Cache,Servervalues that change between endpoints, or aConnection: keep-aliveon a path where you expected none.
Testing for a desync, in order
- Establish that there are two hops. No proxy, no smuggling.
- Timing-probe for CL.TE and then TE.CL. Two requests, both non-destructive.
- If neither hangs, try the TE.TE obfuscations - each is one more request.
- If the target speaks HTTP/2, test the downgrade variants: a contradictory
content-length, an illegaltransfer-encoding, and CRLF in a header value. - Confirm carefully, on an endpoint nobody else is hitting, and observe the effect on your own next request first.
- Choose the primitive that matches the objective: bypass for a blocked path, capture for a victim's cookie, cache poisoning for persistence.
And keep it in proportion. Smuggling is the most powerful HTTP bug class there is, and also the one most likely to eat an evening on a target that has one server. Confirm the two-hop architecture before you spend the time.