HTTP Request Smuggling Code Review Guide
Table of Contents
Introduction
HTTP Request Smuggling is a technique that exploits disagreements between how front-end (proxy/load balancer) and back-end servers determine the boundaries of HTTP requests. When these two systems interpret the same stream of bytes differently, an attacker can "smuggle" a hidden request inside a legitimate one—causing it to be processed as a separate request by the back-end.
First documented in 2005, request smuggling has resurged as a critical attack class thanks to modern multi-tier architectures. Nearly every production web application sits behind at least one reverse proxy, CDN, or load balancer, creating the parsing differentials that smuggling exploits.
HTTP Request Smuggling Attack Flow
Crafted requestUses Content-LengthUses Transfer-EncodingSmuggled prefixBypass access controlsPoison web cacheHijack other users' requestsSteal credentialsTrigger reflected XSSOpen redirect chainsWhy This Matters
Request smuggling is notoriously difficult to detect with traditional security tools. It bypasses WAFs, poisons caches, hijacks other users' sessions, and can chain into account takeover—all without leaving obvious traces in application logs. Understanding it from a code-review perspective is essential for defense.
Real-World Scenario
Consider a typical production setup: an Nginx reverse proxy forwards requests to a Node.js application server. The proxy uses Content-Length to determine request boundaries, while the application server prioritizes Transfer-Encoding: chunked.
Nginx Proxy Configuration
1upstream backend {
2 server app-server:3000;
3}
4
5server {
6 listen 80;
7
8 location / {
9 proxy_pass http://backend;
10 proxy_set_header Host $host;
11 proxy_set_header X-Real-IP $remote_addr;
12 # Nginx forwards both CL and TE headers as-is
13 }
14}An attacker sends a single HTTP request containing both Content-Length and Transfer-Encoding headers. Nginx reads the Content-Length to decide how many bytes to forward. The Node.js back-end reads Transfer-Encoding: chunked to parse the body—and stops reading earlier, leaving the remaining bytes in the socket buffer.
Smuggling Attempt
1POST / HTTP/1.1
2Host: vulnerable-app.com
3Content-Length: 30
4Transfer-Encoding: chunked
5
60
7
8GET /admin HTTP/1.1
9X: ignoreThe back-end processes the chunked body (terminating at 0\r\n\r\n) as request #1. The leftover bytes—GET /admin HTTP/1.1...—sit in the connection buffer. When the next legitimate user's request arrives on the same connection, the back-end prepends the smuggled prefix to it, causing the victim to receive the response to GET /admin.
What causes the parsing differential in HTTP request smuggling?
How Smuggling Works
HTTP/1.1 provides two mechanisms for specifying the length of a request body. Content-Length declares the body size in bytes, while Transfer-Encoding: chunked uses a series of size-prefixed chunks terminated by a zero-length chunk. RFC 7230 states that if both headers are present, Transfer-Encoding takes precedence—but not all implementations follow this rule.
Request Length Mechanisms
| Header | Method | Termination |
|---|---|---|
| Content-Length | Fixed byte count | After exactly N bytes |
| Transfer-Encoding: chunked | Chunked encoding | Zero-length chunk (0\r\n\r\n) |
| Both present (per RFC) | TE takes precedence | Depends on implementation |
The three classic smuggling variants arise from which header each server honors:
Smuggling Variants
| Variant | Front-End Uses | Back-End Uses | Scenario |
|---|---|---|---|
| CL.TE | Content-Length | Transfer-Encoding | Front-end forwards more bytes than the back-end consumes |
| TE.CL | Transfer-Encoding | Content-Length | Front-end forwards fewer bytes; back-end reads past the chunk boundary |
| TE.TE | Transfer-Encoding | Transfer-Encoding | Both use TE, but one can be tricked into ignoring it via obfuscation |
Connection Reuse Is Key
Request smuggling only works when the front-end and back-end reuse the same TCP connection for multiple requests (HTTP keep-alive or pipelining). If every request uses a fresh connection, leftover bytes are discarded when the socket closes. This is why smuggling is most prevalent behind reverse proxies and CDNs that maintain persistent backend connections.
According to RFC 7230, which header should take precedence when both Content-Length and Transfer-Encoding are present?
CL.TE Attacks
In a CL.TE attack, the front-end server uses Content-Length and the back-end uses Transfer-Encoding: chunked. The front-end reads the full body according to Content-Length and forwards it. The back-end parses chunked encoding, stops at the zero-length terminator, and treats remaining bytes as the start of a new request.
CL.TE Parsing Differential
Front-End (uses Content-Length)
Sees 13 bytes → forwards entire body including "SMUGGLED"
Back-End (uses Transfer-Encoding)
Reads until chunk "0" → treats "SMUGGLED" as start of next request
CL.TE Smuggling Payload
1POST / HTTP/1.1
2Host: vulnerable-app.com
3Content-Length: 35
4Transfer-Encoding: chunked
5
60
7
8GET /admin HTTP/1.1
9Host: vulnerable-app.com
10What happens step by step:
1. The front-end reads Content-Length: 35 and forwards 35 bytes of body to the back-end.
2. The back-end parses Transfer-Encoding: chunked and reads the body: chunk size 0 signals end of the chunked body.
3. The remaining bytes (GET /admin HTTP/1.1...) are left in the connection buffer.
4. The back-end treats these leftover bytes as the beginning of the next HTTP request.
5. When another user's request arrives on the same connection, the back-end prepends the smuggled prefix to it.
CL.TE — Targeting Another User
1Attacker sends:
2POST / HTTP/1.1
3Host: vulnerable-app.com
4Content-Length: 70
5Transfer-Encoding: chunked
6
70
8
9POST /account/password HTTP/1.1
10Host: vulnerable-app.com
11Content-Length: 200
12
13password=hacked&csrf=
14
15Next legitimate user's request is appended:
16...csrf=GET / HTTP/1.1
17Host: vulnerable-app.com
18Cookie: session=VICTIM_SESSION_TOKEN
19...
20
21The back-end sees a POST to /account/password with the victim's
22session cookie appended to the body—effectively changing the
23victim's password using their own session.Silent and Devastating
CL.TE attacks are particularly dangerous because the front-end sees a perfectly normal request. Security controls like WAFs that inspect the front-end's view of the request are completely blind to the smuggled content. The attack leaves no trace in front-end access logs.
In a CL.TE attack, the front-end forwards the full body based on Content-Length. What does the back-end do with the extra bytes?
TE.CL Attacks
In a TE.CL attack, the front-end uses Transfer-Encoding: chunked while the back-end uses Content-Length. The front-end reads the full chunked body and forwards it. The back-end reads only Content-Length bytes, leaving the remainder in the buffer as a smuggled request prefix.
TE.CL Smuggling Payload
1POST / HTTP/1.1
2Host: vulnerable-app.com
3Content-Length: 4
4Transfer-Encoding: chunked
5
65c
7GET /admin HTTP/1.1
8Host: vulnerable-app.com
9Content-Type: text/plain
10
11x=
120
13
14Step by step:
1. The front-end parses Transfer-Encoding: chunked. It reads chunk 5c (92 bytes), then chunk 0 (terminator). The entire body is forwarded to the back-end.
2. The back-end reads Content-Length: 4 and consumes only the first 4 bytes (5c\r\n).
3. The remaining bytes—starting with GET /admin HTTP/1.1—remain in the buffer.
4. The back-end interprets them as a separate request, achieving smuggling.
TE.CL Is Trickier to Construct
TE.CL payloads require carefully calculated chunk sizes. The hex chunk size must exactly match the byte count of the smuggled content. Miscounting even by one byte will cause the back-end to misparse the request, often resulting in a timeout or error rather than successful smuggling.
TE.CL — Bypassing Front-End Security
1Scenario: Front-end WAF blocks requests to /admin
2
3Attacker sends:
4POST /search HTTP/1.1 ← WAF sees a harmless /search request
5Host: app.com
6Content-Length: 4
7Transfer-Encoding: chunked
8
960
10POST /admin/delete-user HTTP/1.1
11Host: app.com
12Content-Type: application/x-www-form-urlencoded
13
14id=1
150
16
17Result:
18- Front-end WAF inspects the request to /search — passes
19- Back-end reads only CL bytes from the body
20- Leftover "POST /admin/delete-user..." becomes a separate request
21- Back-end processes the admin action without WAF inspectionWhy are TE.CL payloads more difficult to construct than CL.TE payloads?
Finding Vulnerable Code
During code review, request smuggling vulnerabilities are found not in the application code itself, but in how the infrastructure is configured. Focus on proxy configurations, load balancer settings, and HTTP parsing libraries.
Where to Look During Code Review
| Component | What to Check | Risk Signal |
|---|---|---|
| Reverse proxy config | How CL and TE headers are forwarded | Passing both headers without normalization |
| Load balancer settings | HTTP/1.1 vs HTTP/2 downgrading | Downgrading H2 to H1.1 for backend connections |
| Backend HTTP parser | How ambiguous requests are handled | Lenient parsing that tolerates malformed headers |
| Connection pooling | Keep-alive and connection reuse | Persistent connections between proxy and backend |
| Custom HTTP handling | Manual header parsing in middleware | Hand-rolled parsers that don't follow RFC 7230 |
Vulnerable Nginx Configuration
1# DANGEROUS: Forwards both CL and TE without normalization
2upstream backend {
3 server app:3000;
4 keepalive 64; # Persistent connections — required for smuggling
5}
6
7server {
8 listen 80;
9
10 location / {
11 proxy_pass http://backend;
12 proxy_http_version 1.1;
13 proxy_set_header Connection ""; # Enables keep-alive to backend
14 # No stripping of Transfer-Encoding or Content-Length
15 }
16}Vulnerable HAProxy Configuration
1# DANGEROUS: Uses HTTP tunnel mode which disables parsing
2frontend http_front
3 bind *:80
4 default_backend http_back
5 option http-tunnel # Passes raw bytes — no header normalization
6
7backend http_back
8 server app 127.0.0.1:3000Vulnerable Node.js Custom Parser
1const http = require('http');
2const net = require('net');
3
4// DANGEROUS: Custom HTTP proxy that forwards raw bytes
5const proxy = net.createServer((clientSocket) => {
6 const backendSocket = net.connect(3000, '127.0.0.1');
7
8 // Raw byte forwarding — no request boundary validation
9 clientSocket.pipe(backendSocket);
10 backendSocket.pipe(clientSocket);
11});
12
13proxy.listen(8080);Checking for Lenient Parsing in Go
1// DANGEROUS: Custom server that doesn't reject ambiguous requests
2func handler(w http.ResponseWriter, r *http.Request) {
3 // Go's net/http rejects requests with both CL and chunked TE
4 // by default, but custom servers might not
5 body, _ := io.ReadAll(r.Body)
6 // ...
7}
8
9// SAFE: Go's default HTTP server rejects smuggling attempts
10// But be cautious with:
11// 1. Third-party HTTP libraries (fasthttp, etc.)
12// 2. Custom reverse proxy implementations
13// 3. HTTP/2 → HTTP/1.1 downgrade proxiesReview Infrastructure as Code
Request smuggling vulnerabilities often hide in Terraform modules, Kubernetes ingress annotations, Docker Compose files, and CDN configurations. During code review, always examine infrastructure definitions alongside application code. A single proxy_http_version 1.1 with keepalive in an Nginx config can make the entire application vulnerable.
Which infrastructure pattern is most likely to enable request smuggling?
Prevention Techniques
Preventing request smuggling requires ensuring that every component in the request pipeline agrees on request boundaries. This is primarily an infrastructure and configuration concern, but application-level defenses add valuable defense in depth.
Prevention Strategies
| Technique | Effectiveness | Implementation |
|---|---|---|
| Use HTTP/2 end-to-end | Highest | HTTP/2 uses binary framing, eliminating CL/TE ambiguity |
| Normalize headers at the proxy | High | Strip or reject requests with both CL and TE |
| Disable connection reuse | High | Use separate connections per request (performance cost) |
| Reject ambiguous requests | High | Return 400 for requests with both CL and TE |
| Use strict HTTP parsers | High | Configure servers to reject malformed requests |
| Uniform server software | Medium | Same HTTP parser on front-end and back-end |
Safe Nginx Configuration
1upstream backend {
2 server app:3000;
3}
4
5server {
6 listen 443 ssl http2; # Accept HTTP/2 from clients
7
8 location / {
9 proxy_pass http://backend;
10 proxy_http_version 1.1;
11 proxy_set_header Connection "close"; # No keep-alive to backend
12
13 # Normalize Transfer-Encoding: remove it and use Content-Length
14 proxy_set_header Transfer-Encoding "";
15 }
16}Safe HAProxy Configuration
1frontend http_front
2 bind *:443 ssl crt /etc/ssl/cert.pem alpn h2,http/1.1
3 default_backend http_back
4 # Use httpclose to disable keep-alive to backend
5 option httpclose
6
7backend http_back
8 option httpchk GET /health
9 server app 127.0.0.1:3000
10
11 # HAProxy 2.x+ normalizes requests by default
12 # Ensure you're not using 'option http-tunnel'Application-Level Defense (Express Middleware)
1const express = require('express');
2const app = express();
3
4app.use((req, res, next) => {
5 const hasCL = req.headers['content-length'] !== undefined;
6 const hasTE = req.headers['transfer-encoding'] !== undefined;
7
8 if (hasCL && hasTE) {
9 return res.status(400).json({
10 error: 'Ambiguous request: both Content-Length and Transfer-Encoding present',
11 });
12 }
13
14 next();
15});Best Practice Summary
Use HTTP/2 to backends when possible — binary framing eliminates the CL/TE parsing differential entirely.
Normalize at the edge — configure your reverse proxy or CDN to strip Transfer-Encoding and recalculate Content-Length.
Reject ambiguous requests — any request containing both Content-Length and Transfer-Encoding should return 400.
Disable keep-alive to backends if HTTP/2 is not an option — this eliminates the connection reuse that smuggling depends on.
Monitor for anomalies — unexpected 400/502 errors, request timeouts, and mismatched access logs between front-end and back-end can signal smuggling attempts.