Inconsistent Interpretation of HTTP Requests ('HTTP Request Smuggling')
Description
HTTP Request Smuggling occurs when front-end and back-end servers interpret HTTP request boundaries differently. This discrepancy arises from ambiguous handling of Content-Length and Transfer-Encoding headers, or malformed HTTP requests. Attackers exploit this to "smuggle" a hidden request that bypasses security controls, accesses other users' sessions, or poisons web caches. The technique exploits the fundamental trust relationship between chained HTTP servers.
Risk
HTTP Request Smuggling can bypass web application firewalls, access control lists, and authentication mechanisms. Attackers can hijack other users' requests, capture credentials, or poison caches to serve malicious content to all users. The vulnerability has led to critical exploits in major platforms. Detection is difficult because attacks look like legitimate traffic. Modern infrastructure with load balancers, CDNs, and reverse proxies creates many opportunities for smuggling.
Solution
Ensure all servers in the chain interpret HTTP requests identically. Configure front-end servers to normalize ambiguous requests. Reject requests with both Content-Length and Transfer-Encoding headers. Use HTTP/2 end-to-end where possible (it has explicit length framing). Disable connection reuse if necessary. Implement strict HTTP parsing that rejects malformed requests. Monitor for unusual request patterns. Use web application firewalls that detect smuggling attempts.
Common Consequences
| Impact | Details |
|---|---|
| Access Control | Scope: Security Bypass Smuggled requests bypass WAFs, authentication, and access controls. |
| Confidentiality | Scope: Request Hijacking Attackers can capture and read other users' requests including credentials. |
| Integrity | Scope: Cache Poisoning Malicious responses can be cached and served to all users. |
Example Code + Solution Code
Vulnerable Scenarios
# CL.TE Attack (Front-end uses Content-Length, Back-end uses Transfer-Encoding)
# VULNERABLE: Ambiguous request
POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 13
Transfer-Encoding: chunked
0
SMUGGLED
# Front-end sees Content-Length: 13, forwards "0\r\n\r\nSMUGGLED"
# Back-end sees chunked encoding, processes "0\r\n\r\n" as end,
# "SMUGGLED" becomes start of next request!
# TE.CL Attack (Front-end uses Transfer-Encoding, Back-end uses Content-Length)
# VULNERABLE: Smuggling via TE.CL
POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 3
Transfer-Encoding: chunked
8
SMUGGLED
0
# Front-end processes chunked, sends everything
# Back-end reads only 3 bytes ("8\r\n"), leaves "SMUGGLED..." for next request
# TE.TE Attack (Obfuscated Transfer-Encoding)
# VULNERABLE: Transfer-Encoding obfuscation
POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 4
Transfer-Encoding: chunked
Transfer-Encoding: x
5c
GPOST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 15
x=1
0
# Servers disagree on which Transfer-Encoding to use
# VULNERABLE: Server not handling ambiguous headers
from http.server import HTTPServer, BaseHTTPRequestHandler
class VulnerableHandler(BaseHTTPRequestHandler):
def do_POST(self):
# Reading Content-Length without checking Transfer-Encoding
content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length)
# If Transfer-Encoding: chunked was also present,
# the body interpretation is inconsistent
self.process_request(body)
// VULNERABLE: Express with inconsistent parsing
const express = require('express');
const app = express();
// Default body parser might not handle all edge cases
app.use(express.json());
// Proxy forwarding without normalization
app.use('/api', (req, res) => {
// Simply forwarding without sanitizing headers
proxy.web(req, res, { target: 'http://backend' });
});
Fixed Code
# SAFE: Strict HTTP header handling
from http.server import HTTPServer, BaseHTTPRequestHandler
class SecureHandler(BaseHTTPRequestHandler):
def do_POST(self):
# Check for ambiguous headers
content_length = self.headers.get('Content-Length')
transfer_encoding = self.headers.get('Transfer-Encoding')
# Reject requests with both headers
if content_length and transfer_encoding:
self.send_error(400, "Ambiguous request: both CL and TE present")
return
# Handle Transfer-Encoding
if transfer_encoding:
if transfer_encoding.lower() != 'chunked':
self.send_error(400, "Unsupported Transfer-Encoding")
return
body = self.read_chunked_body()
elif content_length:
try:
length = int(content_length)
if length < 0 or length > MAX_BODY_SIZE:
self.send_error(400, "Invalid Content-Length")
return
body = self.rfile.read(length)
except ValueError:
self.send_error(400, "Invalid Content-Length")
return
else:
body = b''
self.process_request(body)
def read_chunked_body(self):
"""Safely read chunked body."""
body = b''
while True:
line = self.rfile.readline()
try:
chunk_size = int(line.strip(), 16)
except ValueError:
raise ValueError("Invalid chunk size")
if chunk_size == 0:
break
if chunk_size > MAX_CHUNK_SIZE:
raise ValueError("Chunk too large")
body += self.rfile.read(chunk_size)
self.rfile.read(2) # CRLF
return body
# SAFE: Nginx configuration to prevent smuggling
http {
# Normalize requests before forwarding
# Reject requests with both Content-Length and Transfer-Encoding
map $http_transfer_encoding $reject_request {
default 0;
"~*chunked" $http_content_length;
}
server {
listen 443 ssl http2;
# Use HTTP/2 to backend if possible
location / {
# Reject ambiguous requests
if ($reject_request) {
return 400;
}
# Normalize Transfer-Encoding
proxy_http_version 1.1;
proxy_set_header Connection "";
# Remove potentially dangerous headers
proxy_set_header Transfer-Encoding "";
# Recalculate Content-Length
proxy_set_header Content-Length $content_length;
proxy_pass http://backend;
}
}
}
// SAFE: Express with strict HTTP handling
const express = require('express');
const app = express();
// Middleware to detect smuggling attempts
function detectSmuggling(req, res, next) {
const contentLength = req.headers['content-length'];
const transferEncoding = req.headers['transfer-encoding'];
// Reject requests with both headers
if (contentLength && transferEncoding) {
return res.status(400).send('Bad Request: Ambiguous headers');
}
// Reject malformed Transfer-Encoding
if (transferEncoding && transferEncoding.toLowerCase() !== 'chunked') {
return res.status(400).send('Bad Request: Invalid Transfer-Encoding');
}
// Check for header smuggling via whitespace/null bytes
for (const [key, value] of Object.entries(req.headers)) {
if (/[\r\n\0]/.test(key) || /[\r\n\0]/.test(value)) {
return res.status(400).send('Bad Request: Invalid header');
}
}
next();
}
app.use(detectSmuggling);
// Normalize headers before proxying
const { createProxyMiddleware } = require('http-proxy-middleware');
app.use('/api', createProxyMiddleware({
target: 'http://backend:8080',
changeOrigin: true,
onProxyReq: (proxyReq, req, res) => {
// Remove potentially dangerous headers
proxyReq.removeHeader('transfer-encoding');
// Ensure Content-Length is accurate
if (req.body) {
const bodyData = JSON.stringify(req.body);
proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
}
}
}));
// SAFE: Java servlet filter for smuggling prevention
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
public class AntiSmugglingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String contentLength = httpRequest.getHeader("Content-Length");
String transferEncoding = httpRequest.getHeader("Transfer-Encoding");
// Reject ambiguous requests
if (contentLength != null && transferEncoding != null) {
httpResponse.sendError(400, "Ambiguous request headers");
return;
}
// Validate Content-Length
if (contentLength != null) {
try {
long length = Long.parseLong(contentLength);
if (length < 0) {
httpResponse.sendError(400, "Invalid Content-Length");
return;
}
} catch (NumberFormatException e) {
httpResponse.sendError(400, "Invalid Content-Length");
return;
}
}
// Validate Transfer-Encoding
if (transferEncoding != null) {
if (!transferEncoding.equalsIgnoreCase("chunked")) {
httpResponse.sendError(400, "Unsupported Transfer-Encoding");
return;
}
}
// Check for duplicate headers (could indicate smuggling)
if (hasDuplicateHeader(httpRequest, "Content-Length") ||
hasDuplicateHeader(httpRequest, "Transfer-Encoding")) {
httpResponse.sendError(400, "Duplicate critical headers");
return;
}
chain.doFilter(request, response);
}
private boolean hasDuplicateHeader(HttpServletRequest request, String headerName) {
java.util.Enumeration<String> values = request.getHeaders(headerName);
int count = 0;
while (values.hasMoreElements()) {
values.nextElement();
count++;
if (count > 1) return true;
}
return false;
}
}
// SAFE: Go reverse proxy with smuggling prevention
package main
import (
"net/http"
"net/http/httputil"
"net/url"
"strings"
)
func antiSmugglingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check for both Content-Length and Transfer-Encoding
hasContentLength := r.Header.Get("Content-Length") != ""
hasTransferEncoding := r.Header.Get("Transfer-Encoding") != ""
if hasContentLength && hasTransferEncoding {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
// Check for obfuscated Transfer-Encoding
for _, te := range r.Header.Values("Transfer-Encoding") {
normalized := strings.ToLower(strings.TrimSpace(te))
if normalized != "chunked" && normalized != "" {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
}
// Check for header injection
for key, values := range r.Header {
if strings.ContainsAny(key, "\r\n\x00") {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
for _, v := range values {
if strings.ContainsAny(v, "\r\n\x00") {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
}
}
next.ServeHTTP(w, r)
})
}
func main() {
backend, _ := url.Parse("http://backend:8080")
proxy := httputil.NewSingleHostReverseProxy(backend)
// Modify proxy director to normalize requests
originalDirector := proxy.Director
proxy.Director = func(r *http.Request) {
originalDirector(r)
// Remove ambiguous headers
r.Header.Del("Transfer-Encoding")
// Use HTTP/1.1 with Connection: close to prevent smuggling
r.Header.Set("Connection", "close")
}
http.Handle("/", antiSmugglingMiddleware(proxy))
http.ListenAndServe(":8080", nil)
}
Exploited in the Wild
Capital One WAF Bypass (2019)
HTTP request smuggling was used to bypass AWS WAF protections, contributing to a major data breach affecting over 100 million customers.
Multiple CDN Vulnerabilities (2019-2020)
Researchers demonstrated HTTP request smuggling attacks against major CDNs including Akamai, Cloudflare, and others, enabling cache poisoning and request hijacking.
Slack Bug Bounty (2019)
A critical HTTP request smuggling vulnerability in Slack's infrastructure allowed attackers to hijack other users' requests.
Tools to test/exploit
-
smuggler — HTTP request smuggling detection tool.
-
Burp Suite HTTP Request Smuggler — Burp extension.
-
h2csmuggler — HTTP/2 cleartext smuggling tool.
-
http-desync-attacks-request-smuggling-reborn — Research paper with techniques.
CVE Examples
-
CVE-2019-15605 — Node.js HTTP smuggling vulnerability.
-
CVE-2020-8287 — Node.js HTTP request smuggling.
-
CVE-2021-22959 — llhttp HTTP request smuggling.
References
-
MITRE. "CWE-444: Inconsistent Interpretation of HTTP Requests." https://cwe.mitre.org/data/definitions/444.html
-
PortSwigger. "HTTP Request Smuggling." https://portswigger.net/web-security/request-smuggling