CL.TE request smuggling is the cleanest version of a request desync and the easiest one to picture. The name states the setup: the front end trusts the Content-Length header and the back end trusts Transfer-Encoding: chunked. Both read the same bytes off one shared connection, and they disagree about where the body of a request ends. That disagreement is the whole bug. If two servers reading one stream differently is a new idea, start with our primer on http request smuggling and then come back for the byte math.
How CL.TE request smuggling splits one request in two
A front end sits in front of your application: a proxy, a load balancer, or a content delivery node. It reads each request, maybe filters it, then forwards it to the back end over a connection the two of them keep open and reuse. The back end reads that stream and slices it back into requests on its own. As long as both slice at the same byte, nothing goes wrong. CL.TE is the case where the front end measures the body with Content-Length and the back end measures it with the chunked encoding, so they cut at different places.
Here is a request built to make them disagree. It carries both length headers on purpose:
POST / HTTP/1.1 Host: acme-notes.example Content-Length: 6 Transfer-Encoding: chunked 0 GET /admin HTTP/1.1 Host: acme-notes.example Foo: x
Read it once as the front end. The front end honors Content-Length: 6, so it reads exactly six bytes of body after the blank line. Those six bytes are the 0, a carriage return, a line feed, and the blank line that follows. To the front end the body is that short string and the request is complete. It forwards every byte it received, because it believes the whole thing is one ordinary POST.
Now read the same bytes as the back end. The back end honors Transfer-Encoding: chunked and ignores Content-Length entirely. It reads the body as chunks. The first chunk header is 0, which is the terminator that marks the end of a chunked body. So the back end decides the body is empty and the POST ends right there. The bytes after the 0 did not disappear. They sit at the front of the back end buffer:
GET /admin HTTP/1.1 Host: acme-notes.example Foo: x
The front end forwarded one request. The back end read one and a fragment. The fragment left over is attacker chosen, and it is waiting for the next person in line.
What the leftover prefix does next
The back end treats those leftover bytes as the start of the next request on the connection. It does not blame the attacker for them. It glues them onto whatever request arrives next. The dangling Foo: x line has no finished value, so it swallows the first line of the victim request that follows, which keeps the smuggled request syntactically valid.
Say a normal user sends a request a moment later:
GET / HTTP/1.1 Host: acme-notes.example Cookie: session=victim-session
What the back end parses is the attacker lines fused with the victim lines. The request it runs is a GET /admin carrying the victim session cookie. The user asked for the home page and instead drove a request the attacker wrote. The damage depends on the app:
- Bypassing front end controls. The front end is often where path rules and header filters live. A smuggled request never passed the front end as a request, so a route it would have blocked reaches the back end unfiltered.
- Stealing a victim request. Craft the prefix to end in a header that expects a long value, and the victim request, cookies and all, gets captured as that value and stored where the attacker can read it back.
- Poisoning the response queue. Once the request boundary is off by one, responses fall out of step with who asked for them, and a later user receives a reply meant for a different request. We cover that in our note on response queue poisoning.
Why CL.TE is the textbook case
CL.TE is the variant people learn first because the length math is trivial. The smuggled chunk terminator is tiny and the Content-Length only has to cover it. The mirror image, where the front end honors chunked and the back end honors Content-Length, needs more careful sizing and is worth reading as its own case in TE.CL request smuggling. There is also the variant where both servers support chunked and you obfuscate the header to break their agreement, which lives in TE.TE request smuggling.
The fix is not a payload filter. There is no bad string to match, because every byte in the attack is valid on its own. The defense is to stop the two servers from ever reading length differently. A request that carries both Content-Length and Transfer-Encoding should be rejected, not interpreted, and the connection closed afterward so no leftover can wait for a victim. The front end should also normalize every request into one unambiguous form with a single length header it computed itself. These framing bugs sit next to classic input handling flaws, which is why we file them under injection and input.
A pattern match can tell you that a request carries two length headers. It cannot tell you whether the specific front end and back end in your path actually disagree about that request, which is the only thing that makes the desync real. That gap, between a header that looks suspicious and a boundary that is genuinely read two ways, is where real testing has to live. It is why UnboundCompute reasons about whether a desync is reachable in a given chain rather than flagging every request that matches a known shape, which you can read about on our about page. The two servers are sure they agree. The exploit is proof that they do not.
Frequently asked questions
What does CL.TE mean in request smuggling?
CL.TE names which header each server trusts. The front end honors the Content Length header and reads a fixed number of body bytes, while the back end honors Transfer Encoding chunked and reads the body as a series of chunks. Because they measure the body differently, they disagree about where one request ends and the next begins, and that disagreement is the desync an attacker uses to hide a second request inside the first.
Why does the chunk terminator matter in a CL.TE attack?
The attacker sends a body that starts with a zero sized chunk, which is the chunked terminator. The back end, reading chunks, sees that zero and decides the body is finished immediately. The front end, counting bytes with Content Length, reads past the zero and forwards everything. The bytes after the zero chunk are left in the back end buffer and become the start of the next request on the shared connection.
What can an attacker do with a CL.TE desync?
Three things, depending on the app. They can reach routes the front end was supposed to block, since the smuggled request never passed the front end as a request. They can capture a victim request, cookies included, by ending the prefix with a header that expects a long value. Or they can poison the response queue so a later user receives a reply meant for a different request.
How do you prevent CL.TE request smuggling?
Do not let the two servers read length differently. Reject any request that carries both a Content Length and a Transfer Encoding header rather than guessing which one wins, and close the connection afterward so no leftover bytes can wait for a victim. Have the front end normalize every request into one unambiguous framing with a single length value it computed itself before forwarding to the back end.
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.
