When Two Servers Disagree#

Almost every production web app has more than one HTTP parser in the request path: a CDN or load balancer at the edge, maybe a reverse proxy, then the application server. For the pipeline to work, all of them must agree on where one request ends and the next begins. HTTP request smuggling exploits the cases where they do not agree, letting an attacker hide a second request inside the first so that the front-end and back-end split the byte stream differently.

Because a keep-alive connection between the front-end and back-end is reused for many users, a smuggled request fragment gets prepended to the next user's request. That is what makes smuggling dangerous: it is not just an attack on the attacker's own session, it poisons other people's traffic.

The Root Cause: Two Ways to Say "How Long"#

HTTP/1.1 gives two mechanisms for the length of a request body:

  • Content-Length (CL): the body is exactly N bytes.
  • Transfer-Encoding: chunked (TE): the body is sent as size-prefixed chunks, ending with a zero-size chunk (0\r\n\r\n).

The spec says if both are present, Transfer-Encoding wins and Content-Length must be ignored. The vulnerability appears when the two servers in the chain make different choices, or when one can be tricked into ignoring a Transfer-Encoding header it should honour. Then the front-end thinks the request ends in one place and the back-end thinks it ends in another, and the leftover bytes become the start of the next request on that connection.

The Variants#

  • CL.TE: the front-end uses Content-Length, the back-end uses Transfer-Encoding. The front-end forwards the whole thing as one request; the back-end sees a chunked body that ends early and treats the remainder as a new request.
  • TE.CL: the reverse. The front-end honours chunked encoding; the back-end honours Content-Length.
  • TE.TE: both support Transfer-Encoding, but one is fooled into ignoring it by an obfuscated header, which collapses the situation back into CL.TE or TE.CL. Obfuscations that have worked against various servers include a space before the colon, an unusual value like chunked with trailing junk, a duplicated header, or a tab or vertical whitespace character:
Transfer-Encoding: chunked
Transfer-Encoding : chunked
Transfer-Encoding:\tchunked
Transfer-Encoding: xchunked
  • CL.0 and H2.CL / H2.TE: variants where the back-end ignores the body length entirely, or where an HTTP/2 front-end rewrites requests to HTTP/1.1 for the back-end and reintroduces the ambiguity during that downgrade.

A CL.TE Payload, Explained#

A textbook CL.TE probe looks like this on the wire (\r\n shown as line breaks):

POST / HTTP/1.1
Host: example.com
Content-Length: 6
Transfer-Encoding: chunked

0

G

Reading it as each server does:

  • The front-end trusts Content-Length: 6. The body is the six bytes 0\r\n\r\nG (the 0, two CRLFs, and the letter G). It forwards all of it as one complete request.
  • The back-end trusts Transfer-Encoding: chunked. It reads the 0 chunk as "end of body" and stops. The trailing G is left in the connection buffer as the first byte of the next request.

When the next real user's request arrives on that reused connection, it gets prefixed with G, so the back-end sees GPOST / HTTP/1.1..., which mangles or hijacks that request. A real exploit puts a full smuggled request after that prefix rather than a single stray byte.

What Attackers Achieve#

Smuggled requests slip past the front-end's logging, authentication and rate limiting, because the front-end never saw them as separate requests. Attackers use that to:

  • Reach restricted endpoints: smuggle a request to /admin that the front-end would have blocked.
  • Capture other users' requests: smuggle a request whose body is large enough to swallow the next user's request, then read it back, harvesting their cookies or CSRF tokens.
  • Poison the shared cache, turning a single request into a stored payload served to everyone. See web cache poisoning.
  • Turn a reflected input into stored XSS by making the front-end attribute a malicious response to a victim's request.

Detecting It Safely#

Smuggling tests can break real users' requests, so they must be done carefully and only with authorisation. The standard safe technique is a timing-based probe that, run in the right order, does not poison another connection:

  • Send a request that, under a CL.TE or TE.CL desync, leaves the back-end waiting for more body bytes that never arrive. If the server hangs for several seconds before timing out, that delay is a strong signal of a length disagreement, and no other user's request is affected because the incomplete request stays on your own socket.

For example, this is the CL.TE timing probe:

POST / HTTP/1.1
Host: example.com
Transfer-Encoding: chunked
Content-Length: 4

1
A
X

A front-end that trusts Content-Length: 4 forwards only 1\r\nA and drops the X. A back-end that reads chunked gets the 1-byte chunk A and then keeps waiting for the rest of the chunked body, because the terminating 0 chunk never arrives, so the connection stalls until it times out. If both servers agree on the length, the request is either rejected or answered immediately. The measured difference is the tell.

The matching TE.CL probe flips the roles:

POST / HTTP/1.1
Host: example.com
Transfer-Encoding: chunked
Content-Length: 6

0

X

A front-end that honours chunked encoding stops at the 0 chunk and forwards only 0\r\n\r\n (5 bytes), dropping the X; a back-end that trusts Content-Length: 6 waits for the sixth byte that never comes. Run the CL.TE probe first: if the target is actually CL.TE-vulnerable, the TE.CL probe's leftover X would be queued in front of the next request on that back-end connection and could disrupt another user.

Burp Suite's HTTP Request Smuggler extension automates these probes, including the CRLF and header obfuscations, and confirms a desync with follow-up requests rather than by poisoning production traffic. Do this only against systems you own or are explicitly authorised to test; on PortSwigger's Web Security Academy labs you can practise every variant legally.

Defences#

The reliable fixes remove the ambiguity rather than trying to detect bad payloads:

  • Normalise or reject at the edge. Treat a request that contains both Content-Length and Transfer-Encoding, or a malformed/obfuscated Transfer-Encoding, as hostile and reject it with a 400. Do not "helpfully" rewrite it.
  • Use HTTP/2 end to end. HTTP/2 carries length in the frame layer, so the CL/TE ambiguity does not exist within it. The catch is downgrade: if your HTTP/2 front-end translates to HTTP/1.1 for the back-end, it can reintroduce smuggling unless it rebuilds the request strictly. Prefer HTTP/2 to the origin too.
  • Disable connection reuse to the back-end where feasible. If each back-end connection serves one request, a smuggled remainder has no next victim on it. This costs performance, so it is a mitigation, not a first choice.
  • Use the same, up-to-date server software at both layers, so their parsing agrees, and keep proxies patched, since many smuggling bugs are parser bugs the vendor has since fixed.

Why This Matters Beyond Smuggling#

Request smuggling is the clearest example of a general principle: when two systems interpret the same bytes differently, an attacker lives in the gap. The same shape drives cache poisoning, some SSRF filter bypasses, and parser-differential bugs generally. Whenever your architecture has two things that must agree on the meaning of input, that agreement is a security boundary worth testing.

Practice#

On the PortSwigger Web Security Academy, work through the CL.TE and TE.CL labs, then the obfuscation and HTTP/2 downgrade labs. For each, write down which server made which choice and which single header change fixed it. Then check your own stack: does your edge reject a request carrying both Content-Length and Transfer-Encoding? Send one (to your own staging system) and see what status you get back.