TE.CL Request Smuggling

TE.CL Request Smuggling

Written by

in

TE.CL request smuggling is the mirror image of the classic desync, and the one where you have to do real arithmetic. Here the front end honors Transfer-Encoding: chunked and the back end honors Content-Length. The roles from the common case are swapped, so the attacker frames the smuggled request as a chunk the front end reads in full, while the back end, counting a small Content-Length, stops early and leaves the rest behind. If request desync is new to you, our primer on http request smuggling sets up the two server disagreement this builds on.

How TE.CL request smuggling frames the prefix

The front end reads the body as a chunked stream and the back end reads it as a fixed number of bytes. To exploit that, the attacker sends one chunk whose declared size covers the entire smuggled request, followed by the zero terminator. The front end, reading chunks, consumes all of it and forwards the lot. The back end, reading Content-Length, takes only the first few bytes and treats everything after as a brand new request.

A TE.CL request looks like this:

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

5c
GPOST /admin HTTP/1.1
Host: acme-notes.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 15

x=1
0

Read it as the front end first. It honors chunked, so it reads the chunk size line 5c. That is hexadecimal for 92, meaning the next 92 bytes are one chunk. Those 92 bytes are the whole GPOST /admin block down to and including x=1. After the chunk comes 0, the terminator, so the front end decides the request is finished and forwards every byte to the back end.

Now read it as the back end. It honors Content-Length: 4 and ignores the chunked header. The four bytes after the blank line are 5c followed by a carriage return and a line feed. That is the entire body as far as the back end is concerned, so the POST ends there. Everything from GPOST onward is still in the buffer, waiting to be read as the next request:

GPOST /admin HTTP/1.1
Host: acme-notes.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 15

x=1

The whole exploit hinges on one number. The chunk size has to count the smuggled bytes exactly, or the front end reads a body of the wrong length and the desync falls apart.

Why the length math is unforgiving

The classic case gets away with a tiny body because the front end only has to count a short chunk terminator. TE.CL is stricter. The chunk size you write must equal the exact byte count of the smuggled request, including every header line and the blank line before its body. Add a header, and the count goes up. Change the host name length, and it changes again. Line endings matter too: each line break in an HTTP message is two bytes, a carriage return and a line feed, and if your tooling rewrites them to one byte the count is wrong and the front end reads a body of the wrong size. When that happens, the chunk does not line up with the terminator and the request is either rejected or passed through clean.

This is why TE.CL is fiddly to test by hand. An intercepting proxy that helpfully recomputes Content-Length or normalizes line endings will quietly rewrite the exact bytes the attack depends on. You either turn that behavior off, or you compute the hex chunk size yourself and send the raw bytes unaltered.

The smuggled request stands on its own

There is a structural difference worth noting against the classic case. In a CL.TE attack the leftover is usually an incomplete request that waits to swallow the next victim line. In the TE.CL example above, the smuggled GPOST /admin is a complete request with its own Content-Length: 15 and body, so it does not need to borrow anything from the victim. The back end runs it as a standalone request. That makes TE.CL a clean way to reach a route the front end would have blocked, since the inner request never passed the front end as a request at all.

If you want to capture a victim request instead of just running your own, you leave the final smuggled header expecting a body longer than what you send, so the victim bytes get appended as that body. The sizing is the same discipline, only now the trailing Content-Length is deliberately too large.

Defenses and the sibling variants

The defenses are the same family as the other variants, because the root cause is identical: two servers reading one stream with two different length rules. Reject any request that carries both Content-Length and Transfer-Encoding rather than picking one, and close the connection after. Make the front end normalize each request into a single unambiguous framing before it forwards. The near twin of this bug, where the front end reads Content-Length and the back end reads chunked, is walked byte by byte in CL.TE request smuggling, and the obfuscation driven case is in TE.TE request smuggling. We group all of these under injection and input because they turn on how a parser reads untrusted bytes.

The reason TE.CL rewards careful work is also why a scanner struggles with it. A tool can notice two length headers, but it cannot tell you whether the front end and the back end in your specific path disagree about this exact request, or whether your chunk size counts the bytes the way the live servers do. That is a question you answer by reasoning about the chain and proving the boundary moves, not by matching a signature. It is the kind of thing UnboundCompute is built to work out, by testing whether a desync is actually reachable rather than pattern matching headers, which you can read about on our about page.

Frequently asked questions

How is TE.CL different from CL.TE?

The two servers trade roles. In TE.CL the front end honors Transfer Encoding chunked and reads the body as chunks, while the back end honors Content Length and reads a fixed byte count. In CL.TE it is the other way around. The outcome is the same kind of leftover prefix in the back end buffer, but the request is framed differently and the length math you need to make it work is stricter in the TE.CL direction.

Why does TE.CL need careful length math?

The attacker sends one chunk whose declared hexadecimal size must equal the exact byte count of the entire smuggled request, including every header line and the blank line before its body. If the count is off by even one byte, the front end reads a body of the wrong length and the chunk no longer lines up with its terminator, so the desync fails. Changing a header or a host name forces you to recompute the size.

Why do line endings break TE.CL tests?

Every line break in an HTTP message is two bytes, a carriage return and a line feed, and those bytes count toward the chunk size. If your tooling or an intercepting proxy rewrites line endings to a single byte or recomputes Content Length for you, it changes the exact bytes the attack depends on. You either disable that normalization or compute the chunk size yourself and send the raw bytes unchanged.

What does a TE.CL smuggled request achieve?

Because the smuggled request in the common TE.CL form is complete on its own, with its own length and body, the back end runs it as a standalone request. That reaches a route the front end was meant to block, since the inner request never passed the front end as a request. With a trailing length set too large you can instead capture the next victim request as the body of the smuggled one.


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.