HTTP Request Smuggling: CL.TE and TE.CL Desync

HTTP Request Smuggling: CL.TE and TE.CL Desync - article cover image Web Exploitation
Time it takes to read this article 6 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

HTTP request smuggling exploits a disagreement between two HTTP servers sitting in the same request path — typically a front-end proxy/load balancer/CDN and a back-end origin — about where one HTTP request ends and the next begins. When a client sends a single, carefully constructed request over a connection both servers process, the front end and the back end can each parse a *different* boundary for that request’s body. Whatever is left over gets smuggled: interpreted by the back end as the beginning of the *next* request on that connection — one that, on a connection-reusing proxy, may belong to a completely different user.

The impact scales with what the smuggled prefix can attach to. At minimum it enables security-control bypass (a front-end WAF applies to the request it saw, not the one the back end executes). At worst it enables session hijacking — the attacker’s smuggled prefix is prepended onto the *next* real user’s request on a reused connection, so the response the attacker receives back can reflect the victim’s headers and cookies. It also combines with web cache poisoning: a smuggled request can poison what a shared cache stores, extending impact to every visitor.

Attack Prerequisites

Request smuggling requires a specific chain of infrastructure and parsing behavior to line up:

  • A front end and a back end that both parse HTTP but disagree on framing — most commonly, Content-Length (CL) vs. Transfer-Encoding: chunked (TE) precedence, or malformed chunked encoding.
  • Connection reuse (keep-alive) between front end and back end, so a smuggled remainder from one request lands at the front of the next request the back end reads on that same connection — often from a different client’s traffic multiplexed onto the same pooled connection.
  • The ability to send a raw, custom-framed request — a raw socket, Burp Repeater with “Update Content-Length” disabled, or a dedicated smuggling tool, since normal browsers won’t send deliberately ambiguous framing.
  • HTTP/1.1 (or a downgrade to it) somewhere in the chain — a stack speaking HTTP/2 end-to-end with no downgrade isn’t vulnerable to classic CL.TE/TE.CL, though separate HTTP/2-specific desync variants exist.

How It Works

HTTP/1.1 has two mutually exclusive ways to tell a server where a request body ends: Content-Length states an exact byte count, and Transfer-Encoding: chunked frames the body as length-prefixed chunks terminated by a zero-length chunk. RFC 9112 says that if both are present, Transfer-Encoding should take precedence — but not every implementation follows that, and some ignore Transfer-Encoding if it doesn’t look exactly as expected. When the front end honors one header and the back end honors the other, they compute different byte counts for the body, and the leftover bytes on one side become the start of a phantom request on the other.

CL.TE (front end uses Content-Length, back end uses Transfer-Encoding): the attacker sends a request with both headers. The front end reads exactly Content-Length bytes as the body and forwards the whole thing as one request. The back end, honoring Transfer-Encoding, reads the body as chunks, hits the terminating 0\r\n\r\n, and considers the request *finished* there, even though more bytes are still sitting in the stream. Those trailing bytes are then read as the opening of the next request.

TE.CL is the mirror image: the attacker sends a chunked body the front end correctly parses and forwards in full, but the back end trusts Content-Length instead and reads only that many bytes, leaving remaining chunk data — still valid-looking HTTP request bytes — to be interpreted as the start of the following request. A third variant, TE.TE, arises when both servers nominally honor Transfer-Encoding but one can be tricked into ignoring it via header obfuscation (a trailing space, duplicate header, or unusual casing) that only one parser treats as invalid, falling back to Content-Length.

Vulnerable Code / Configuration

The bug is a parsing mismatch, not application code — it lives in which HTTP stack sits at each hop and how each resolves the CL/TE ambiguity. A front-end reverse proxy combined with an older or misconfigured back-end stack is the classic vulnerable topology:

upstream backend { server 10.0.0.5:8080; keepalive 64; }

server {
    listen 443 ssl;
    location / {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";   # keep-alive reuse to backend
        # BUG: no rejection of requests carrying BOTH Content-Length and
        # Transfer-Encoding, and connections to backend are pooled/reused,
        # so a smuggled remainder lands in front of another client's request.
    }
}
NGINX

On the back end, the vulnerable behavior is which header a given library trusts when both are present, without rejecting the ambiguous request outright as RFC 9112 requires:

# Illustrative simplified parser logic.
def read_body(headers, sock):
    if 'transfer-encoding' in headers:
        return read_chunked(sock)          # back end's choice
    elif 'content-length' in headers:
        return sock.read(int(headers['content-length']))
    # BUG: never checks for BOTH headers being present and rejecting
    # the message as malformed -- silently picks one.
Python

Walkthrough / Exploitation

Probe for CL.TE by sending a request where the front end’s Content-Length says the body is short, but the actual bytes contain a smuggled follow-up that only the back end (honoring Transfer-Encoding) holds onto:

POST / HTTP/1.1
Host: target.com
Content-Length: 13
Transfer-Encoding: chunked

0

SMUGGLED
HTTP

The front end (CL-based) forwards exactly 13 bytes as the complete body. The back end (TE-based) reads the chunked body, sees the 0 terminator chunk, considers the body finished there, and leaves SMUGGLED\n buffered as the start of the next request on the connection.

Turn this into a real attack by smuggling a complete second request designed to be misrouted, e.g. to capture the next victim’s headers on that connection:

POST / HTTP/1.1
Host: target.com
Content-Length: 130
Transfer-Encoding: chunked

0

GET /home HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 15

x=1
HTTP

The smuggled GET /home prefix is left dangling; the next legitimate visitor’s request bytes get appended onto it and misinterpreted, so the victim’s request line and headers end up reflected in a subsequent probe response — the classic way to prove and capture smuggled victim traffic in a lab environment.

Confirmation is more reliably done with timing-based tooling than by trying to observe misrouted content directly:

# smuggler.py -- automated CL.TE/TE.CL/TE.TE probing
python3 smuggler.py -u https://target.com

# Burp Suite: HTTP Request Smuggler extension / built-in desync detection.
# Send via Repeater with 'Update Content-Length' UNCHECKED, then use
# 'Send group in sequence (single connection)': a TE.CL desync causes the
# second of two requests to hang, since the back end waits on bytes the
# front end already consumed as part of the first request's body.
Bash

Note: Timing is the most reliable signal in modern testing: send two requests back-to-back on one connection where the second should return instantly if there’s no desync, but visibly hangs if a CL.TE or TE.CL disagreement exists. PortSwigger’s HTTP Request Smuggler extension automates exactly this differential timing probe.

Opsec: Smuggling tests can genuinely corrupt other users’ sessions on a shared connection pool in production. Test against dedicated lab/staging targets whenever possible, and if testing production is authorized, use short, clearly-scoped probes and monitor for signs the technique affected live traffic before escalating.

Detection and Defense

Request smuggling is a protocol-level ambiguity, so the fix removes the ambiguity rather than filtering payloads:

  • Reject or normalize ambiguous requests at the edge — any request carrying both Content-Length and Transfer-Encoding should be rejected per RFC 9112, not resolved by picking one header.
  • Use a single, consistent HTTP stack/version end-to-end — terminating on HTTP/2 between front end and back end removes CL/TE text-framing ambiguity entirely.
  • Disable or fully normalize front-to-back connection reuse so a malformed body can’t smuggle a phantom request onto a shared connection.
  • Keep front-end and back-end parsing behavior in sync, patching both layers together and preferring hardened, spec-compliant stacks.
  • Run active smuggling scans regularly (smuggler.py, Burp’s HTTP Request Smuggler) against every front/back pairing after changes.

On the detection side: back-end logs showing request lines that don’t correspond to anything a legitimate client sent, unexplained connection timeouts/hangs, and cases where a WAF-blocked payload still reached the application are all signals worth alerting on.

Real-World Impact

HTTP request smuggling was brought to mainstream attention by James Kettle’s “HTTP Desync Attacks” research (Black Hat USA / DEF CON), which demonstrated session hijacking and cache poisoning against production sites behind major CDNs and load balancers, and directly led to PortSwigger’s HTTP Request Smuggler extension and the community smuggler.py tool. Because the technique targets shared connection-pooling infrastructure rather than a single application bug, a confirmed desync at a widely used CDN or proxy can affect every site behind it, and is consistently rated Critical when shown to affect another user’s traffic.

Conclusion

Request smuggling exists because HTTP/1.1 gives two independent, conflicting ways to frame a message body, and any chain with more than one parser resolving that differently creates a desync an attacker can steer. CL.TE and TE.CL are the two canonical forms of that mismatch; the durable fix is removing the ambiguity — reject conflicting headers, normalize at the edge, and prefer one consistent protocol end-to-end — rather than trying to detect individual malicious payloads.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL