<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://ryanwilson.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://ryanwilson.io/" rel="alternate" type="text/html" /><updated>2026-11-12T23:21:52+00:00</updated><id>https://ryanwilson.io/feed.xml</id><title type="html">Ryan Wilson’s Blog</title><subtitle>Network &amp; Security Engineer. My interests are involved in network security, web applicaiton security, and AI security. I write about any and all security, network, or software related topics here and anything else I feel like writing about.</subtitle><entry><title type="html">HTTP Request Smuggling and Desync</title><link href="https://ryanwilson.io/web/application/security/2026/09/24/http-request-smuggling.html" rel="alternate" type="text/html" title="HTTP Request Smuggling and Desync" /><published>2026-09-24T00:23:08+00:00</published><updated>2026-09-24T00:23:08+00:00</updated><id>https://ryanwilson.io/web/application/security/2026/09/24/http-request-smuggling</id><content type="html" xml:base="https://ryanwilson.io/web/application/security/2026/09/24/http-request-smuggling.html"><![CDATA[<h1 id="where-does-the-request-end">Where Does the Request End?</h1>

<h2 id="http-request-smuggling-and-desync">HTTP Request Smuggling and Desync</h2>

<p>HTTP request smuggling starts with a mismatch: a front-end and a back-end disagree about where a request ends. On a reused connection, bytes left over from one request can become part of the next user’s request. This post explains how that happens, what an attacker can do with it, and how to test and defend against it. It’s the same parser-difference problem as DOM Clobbering and Mutation XSS, this time between HTTP servers.</p>

<h2 id="the-disagreement">The disagreement</h2>

<p>In the previous post, DOM Clobbering and Mutation XSS came down to different parts of a browser interpreting the same input differently. Request smuggling is a similar mismatch between HTTP components: usually a proxy, load balancer, or CDN and the back-end server behind it. Here, the input is a stream of bytes sent over a reused TCP connection.</p>

<p>Modern setups almost never let the origin be accessed directly. Requests go through at least one intermediary, and for performance that intermediary keeps a pooled connection to the origin and sends many users’ requests through it one after another. All of that depends on both sides agreeing, byte for byte, about where each request ends.</p>

<p>If they disagree by even a few bytes, the remaining bytes from an attacker’s request become the first bytes of the next user’s request. The attacker never touches the victim. They simply cause the two parsers to count differently.</p>

<p>Both sides need to agree on the boundary. When they don’t, leftover bytes can be mistaken for the start of another request.</p>

<h2 id="how-http11-marks-a-boundary">How HTTP/1.1 marks a boundary</h2>

<p>An HTTP/1.1 message does not have a fixed length. The body is separated in one of two ways, and the whole set of problems lives in the space between them.</p>

<h3 id="content-length-count-the-bytes">Content-Length: count the bytes</h3>

<p>The simplest method states the body length in bytes using a <code class="language-plaintext highlighter-rouge">Content-Length</code> header. The receiver reads that many bytes and then considers the message finished.</p>

<h3 id="transfer-encoding-chunked-read-until-a-zero-chunk">Transfer-Encoding: chunked: read until a zero chunk</h3>

<p>The streaming method sends the body in a series of chunks. Each chunk has a hexadecimal length, a carriage-return line feed, that many bytes, and another line feed. A zero-length chunk ends the body. The receiver does not know the total length beforehand; it reads until it sees the final <code class="language-plaintext highlighter-rouge">0</code> chunk.</p>

<p>```http id=”3a2zqb”
Transfer-Encoding: chunked</p>

<p>b             (next chunk is 0xb = 11 bytes)
hello world   (11 bytes of data)
0             (zero-length chunk: body ends here)
              (trailing CRLF closes the message)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
### The rule and why it is not enough

The specification is clear: if a message contains both a `Content-Length` header and a `Transfer-Encoding: chunked` header, `Transfer-Encoding` wins. The conflicting `Content-Length` cannot be used to determine the message boundary, and the message may instead be rejected [3][4].

A single parser that follows the rules can handle this correctly. The problem starts when a request goes through two parsers. Those parsers might have been written by different teams, in different programming languages, or even years apart. One parser might follow the rule exactly. The other might contain a bug, use a shortcut, or read the rules more strictly or loosely. Now the same bytes can have two different boundaries.

The header that marks the end of a message is itself data that an attacker can send. When two servers do not agree on which delimiter to trust, the attacker can influence where the message ends on each server separately.

## Common desync patterns

Practical smuggling can be described by a few simple patterns. These patterns are named for the header that the front-end (F) and back-end (B) each trust.

### CL.TE: front-end counts bytes, back-end reads chunks

The front-end uses the `Content-Length` header and forwards the entire request. The back-end uses `Transfer-Encoding`, sees a zero chunk early, and considers the request finished. Anything the front-end sent after that zero chunk ends up in the back-end's buffer as orphaned data and is added before the next request on the same connection.

```http id="g6trxp"
POST / HTTP/1.1
Host: target.example
Content-Length: 13
Transfer-Encoding: chunked

0

SMUGGLED
</code></pre></div></div>

<p>The front-end forwards 13 bytes, including <code class="language-plaintext highlighter-rouge">SMUGGLED</code>. The back-end stops reading at the <code class="language-plaintext highlighter-rouge">0</code> chunk. <code class="language-plaintext highlighter-rouge">SMUGGLED</code> stays in the buffer and becomes the beginning of the next user’s request.</p>

<h3 id="tecl-front-end-reads-chunks-back-end-counts-bytes">TE.CL: front-end reads chunks, back-end counts bytes</h3>

<p>This is the mirror image. The front-end respects the chunked encoding and forwards the entire chunked stream. The back-end trusts the <code class="language-plaintext highlighter-rouge">Content-Length</code> header, reads only the number of bytes specified, and leaves the rest of the chunked data unread in the buffer. That leftover data can poison the next request.</p>

<h3 id="tete-both-read-chunks-but-one-is-fooled">TE.TE: both read chunks, but one is fooled</h3>

<p>Both the front-end and back-end support chunked encoding, so the attacker cannot simply rely on one side ignoring the <code class="language-plaintext highlighter-rouge">Transfer-Encoding</code> header. Instead, the attacker hides or changes the header in a way that causes one side to miss it and silently fall back to <code class="language-plaintext highlighter-rouge">Content-Length</code>.</p>

<p>These obfuscations are small and come in many forms: a space before the colon, a tab instead of a space, an unexpected value like <code class="language-plaintext highlighter-rouge">xchunked</code>, a duplicated header, or a leading character. Each form targets differences in how a parser handles unusual input.</p>

<p>```http id=”wtqx1v”
Transfer-Encoding : chunked      (space before colon)
Transfer-Encoding:<TAB>chunked   (tab, not space)
Transfer-Encoding: xchunked      (non-standard value)
Transfer-Encoding: chunked
Transfer-Encoding: cow           (duplicate, second bogus)</TAB></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
One parser treats these variations as valid chunked encoding while the other does not.

### H2.CL and H2.TE: the downgrade reopens the wound

HTTP/2 was meant to solve this problem. It uses binary framing and does not depend on `Content-Length` or `Transfer-Encoding` to mark request boundaries in the same way HTTP/1.1 does.

The problem reappears when a front-end talks HTTP/2 to the client but changes the request to HTTP/1.1 before sending it to the back-end. If that front-end rewrites the request into HTTP/1.1 while accepting an attacker-controlled `Content-Length` or `Transfer-Encoding`, or if it fails to reject carriage-return or line-feed characters hidden inside HTTP/2 header values, the attacker can again control where the HTTP/1.1 message ends on the back-end.

This type of attack, which also includes request splitting through CRLF injection into HTTP/2 headers, is covered in later research on HTTP desynchronization [2].

## What an attacker can do with a smuggled prefix

A smuggled prefix is a building block, not a full exploit. Its power comes from attaching data to a request that the attacker never sent. The common results are:

- **Bypassing front-end controls.** Access controls, WAF rules, and path restrictions enforced at the front-end are applied to the request that the front-end saw. The smuggled prefix is put together at the back-end, so it can reach paths that the front-end would have blocked.

- **Capturing another user's request.** By inserting a prefix that forces the next request's data to appear somewhere the attacker can read back, such as a comment field or reflected search field, the attacker can capture the victim's headers, including session cookies and authorization tokens.

- **Web cache poisoning and response queue desync.** A desync can shift the order of requests and responses on a connection, causing one user to receive another user's response or causing a poisoned response to be cached and served to everyone.

- **Turning a reflected input into a stored, cross-user effect.** Because the prefix travels with victims' requests, a low-value reflected issue can become a way to attack other users sharing that back-end connection.


## Detecting desync safely

Confirming a desync can corrupt real users' requests, so detection should favor methods that show the disagreement without leaving a poisoned prefix in a shared buffer. The main technique is timing, formalized in the desync research that revived the class [1].

### Timing a CL.TE target

Send a request whose framing forces the back-end to wait for bytes that will never arrive. If the front-end uses `Content-Length` and forwards a fixed number of bytes, but the back-end uses `Transfer-Encoding` and is left mid-chunk expecting more, the back-end blocks until it times out.

That delay is the signal. A matched control request with normal framing should return quickly, which helps separate the framing issue from ordinary network delays.

```http id="ojrbmm"
POST / HTTP/1.1
Host: target.example
Transfer-Encoding: chunked
Content-Length: 4

1
A
X
</code></pre></div></div>

<p>The front-end (CL: 4) forwards the bytes and stops. The back-end (TE) is left with an incomplete chunk and waits for the rest. A long, repeatable delay compared with a clean control is the desync signal.</p>

<h3 id="differential-responses">Differential responses</h3>

<p>When timing is inconclusive, a confirmed desync can create a difference that an attacker can see directly: a follow-up request returns a status or body that only makes sense if a prefix attached to it. Because this method can affect real traffic, it belongs in an authorized test window against a controlled account, not in casual probing.</p>

<h3 id="what-defenders-can-watch-in-telemetry">What defenders can watch in telemetry</h3>

<ul>
  <li>Requests carrying both <code class="language-plaintext highlighter-rouge">Content-Length</code> and <code class="language-plaintext highlighter-rouge">Transfer-Encoding</code>, or malformed <code class="language-plaintext highlighter-rouge">Transfer-Encoding</code> values. In a normal client population these should be rare and are worth investigating.</li>
  <li>Back-end request lines that do not match any front-end-issued request. This can be a sign of a prefix assembling at the origin.</li>
  <li>Spikes in back-end read timeouts correlated with specific source addresses, which can be the footprint of timing probes.</li>
</ul>

<h2 id="testing-and-defending-against-desync">Testing and defending against desync</h2>

<p>Testing should establish whether two parsers disagree and what that lets an attacker reach. Defenders should check that requests are parsed consistently and that leftover bytes cannot spill into another user’s request.</p>

<h3 id="testing">Testing</h3>

<ul>
  <li><strong>Map the chain first.</strong> Identify every intermediary: CDN, WAF, load balancer, service mesh sidecar, and origin. Each hop is a parser, and each parser pair is a possible place for disagreement.</li>
  <li><strong>Probe by timing, not by poisoning.</strong> Start with the timing method so detection does not interfere with real traffic. Confirm with a controlled differential only when authorized.</li>
  <li><strong>Do not update Content-Length automatically.</strong> Turn off the client’s automatic header rewriting or the ambiguity you are creating may be removed before it leaves the tool.</li>
  <li><strong>Chain it to real impact.</strong> A confirmed desync is a finding. A captured session or bypassed access control shows the actual impact. Trace the prefix to what it can affect.</li>
</ul>

<h3 id="defending">Defending</h3>

<ul>
  <li><strong>Normalize or reject at the edge.</strong> The front-end should reject requests that carry conflicting framing information or a non-conformant <code class="language-plaintext highlighter-rouge">Transfer-Encoding</code> instead of forwarding them.</li>
  <li><strong>Make both ends parse identically.</strong> Where possible, use the same server software and configuration for the front-end and back-end so there is less room for a parser differential.</li>
  <li><strong>Break the shared buffer.</strong> Disabling reuse of back-end connections removes the way a leftover prefix reaches another user’s request. There is a performance cost, but in some environments that tradeoff may be worth it.</li>
  <li><strong>Speak HTTP/2 to the origin.</strong> End-to-end HTTP/2 removes the HTTP/1.1 framing ambiguity rather than trying to filter every way it can be exploited.</li>
</ul>

<h3 id="signals">Signals</h3>

<table>
  <thead>
    <tr>
      <th>Signal</th>
      <th>What it indicates</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Timing delta between a mid-chunk probe and a clean control</td>
      <td>Primary safe detection signal</td>
    </tr>
    <tr>
      <td>Coexisting <code class="language-plaintext highlighter-rouge">Content-Length</code> and <code class="language-plaintext highlighter-rouge">Transfer-Encoding</code></td>
      <td>Ambiguity by construction</td>
    </tr>
    <tr>
      <td>Obfuscated <code class="language-plaintext highlighter-rouge">Transfer-Encoding</code> (space, tab, junk value, duplicate)</td>
      <td>TE.TE candidate</td>
    </tr>
    <tr>
      <td>HTTP/2 front-end downgrading to HTTP/1.1 at the origin</td>
      <td>H2.CL / H2.TE surface</td>
    </tr>
    <tr>
      <td>Pooled, reused back-end connections shared across users</td>
      <td>The channel a prefix travels on</td>
    </tr>
  </tbody>
</table>

<h2 id="reducing-the-risk-by-design">Reducing the risk by design</h2>

<p>The most reliable defenses make every component agree on the request boundary, or prevent bytes left over on one request from reaching another.</p>

<h3 id="end-to-end-http2-removes-the-ambiguity-outright">End-to-end HTTP/2 removes the ambiguity outright</h3>

<p>HTTP/2 frames messages in the protocol itself using binary frames. It does not rely on <code class="language-plaintext highlighter-rouge">Content-Length</code> or <code class="language-plaintext highlighter-rouge">Transfer-Encoding</code> to mark request boundaries in the same way HTTP/1.1 does. When HTTP/2 is used through every hop to the origin, the traditional CL and TE framing ambiguity is gone.</p>

<p>The remaining risk is the downgrade. When a hop converts the request to HTTP/1.1, the old framing rules come back, and that hop becomes responsible for producing valid, unambiguous HTTP/1.1. The goal is not just to offer HTTP/2 at the edge. It is to avoid downgrading behind it when possible.</p>

<h3 id="strict-rejection-makes-the-boundary-singular">Strict rejection makes the boundary singular</h3>

<p>Where HTTP/1.1 must be used, the front-end can enforce that a request has exactly one valid way to be framed. Reject messages carrying conflicting framing headers, malformed or unsupported <code class="language-plaintext highlighter-rouge">Transfer-Encoding</code> values, embedded carriage returns or line feeds in header values, and duplicate framing headers.</p>

<p>This does not try to decide which of two interpretations is correct. It prevents input with two interpretations from entering the system in the first place.</p>

<p>```text id=”42dvaz”
Reject, do not forward, when:</p>

<p>has(Content-Length) &amp;&amp; has(Transfer-Encoding)   conflicting framing
Transfer-Encoding is malformed or unsupported  obfuscation
header value contains CR or LF                  splitting
duplicate framing headers                       TE.TE / CL.CL
```</p>

<h3 id="connection-per-request-removes-the-delivery-channel">Connection-per-request removes the delivery channel</h3>

<p>Even a successful desync needs a shared connection to carry the orphaned prefix to another request. If the front-end opens a new connection to the back-end for every request, leftover data has no next request to attach to and dies with the connection.</p>

<p>This trades some performance for isolation and can be a strong mitigation where the performance cost is acceptable.</p>

<h3 id="homogeneous-parsing-removes-the-differential">Homogeneous parsing removes the differential</h3>

<p>The problem exists because two independently built parsers can disagree. Reducing the number of different HTTP implementations in the chain, and configuring the remaining ones consistently, reduces the places where a disagreement can hide.</p>

<p>The connection to the previous post is the parser mismatch: browsers can disagree about markup, and HTTP components can disagree about message boundaries. In either case, consistent parsing is more reliable than trying to anticipate every unusual input.</p>

<h2 id="takeaway">Takeaway</h2>

<p>Request smuggling is more dangerous than its simple mechanism suggests. The mechanism is only a few bytes of disagreement about where a message stops. The result can be the ability to rewrite the beginning of someone else’s request, turning a framing issue into a cross-user attack: stolen sessions, bypassed controls, poisoned caches.</p>

<p>The same setup that makes the web fast can create the conditions for this bug: independent intermediaries share pooled connections and may parse requests differently.</p>

<p>For a security team, the rules are fairly simple. List every hop and treat each pair of parsers as a place where two answers might diverge. Detect by timing before risking a poisoning probe, and do not let your tools automatically correct the ambiguity you are testing. On defense, reject ambiguous framing at the edge, prefer end-to-end HTTP/2, break shared back-end connections where the performance tradeoff makes sense, and reduce unnecessary parser differences throughout the chain.</p>

<p>Every hop needs to agree on where a request ends. Reject ambiguous framing, avoid risky downgrades, and review each parser in the request path.</p>

<h2 id="references-and-further-reading">References and further reading</h2>

<blockquote>
  <p><strong>Note on citations:</strong> These references should be verified against the primary sources before publication. Titles, authors, venues, years, and other bibliographic details should be confirmed at the source.</p>
</blockquote>

<ol>
  <li>Kettle, J. “HTTP Desync Attacks: Request Smuggling Reborn.” PortSwigger Research, presented at Black Hat USA and DEF CON, 2019. The work that revived the class and introduced the timing-based detection methodology.</li>
  <li>Kettle, J. “HTTP/2: The Sequel is Always Worse.” PortSwigger Research, Black Hat USA, 2021. HTTP/2 downgrade desync, H2.CL and H2.TE, and request splitting via header injection.</li>
  <li>Linhart, C., Klein, A., Heiled, R., Orrin, S. “HTTP Request Smuggling.” Watchfire, 2005. The original description of the technique.</li>
  <li>Klein, A. Subsequent research on HTTP request smuggling and response splitting across HTTP versions.</li>
  <li>IETF. RFC 9112, “HTTP/1.1” (obsoleting RFC 7230), message framing, <code class="language-plaintext highlighter-rouge">Content-Length</code> and <code class="language-plaintext highlighter-rouge">Transfer-Encoding</code> precedence and rejection rules.</li>
  <li>IETF. RFC 9113, “HTTP/2,” binary framing and message length semantics.</li>
  <li>PortSwigger Web Security Academy. HTTP request smuggling labs and the HTTP Request Smuggler Burp extension.</li>
</ol>]]></content><author><name></name></author><category term="web" /><category term="application" /><category term="security" /><summary type="html"><![CDATA[Where Does the Request End?]]></summary></entry><entry><title type="html">DOM Clobbering and Mutation XSS</title><link href="https://ryanwilson.io/web/application/security/2026/09/03/dom-clobbering-mutation-xss.html" rel="alternate" type="text/html" title="DOM Clobbering and Mutation XSS" /><published>2026-09-03T15:45:31+00:00</published><updated>2026-09-03T15:45:31+00:00</updated><id>https://ryanwilson.io/web/application/security/2026/09/03/dom-clobbering-mutation-xss</id><content type="html" xml:base="https://ryanwilson.io/web/application/security/2026/09/03/dom-clobbering-mutation-xss.html"><![CDATA[<h1 id="two-documents-the-same-bytes">Two Documents, the Same Bytes</h1>

<h2 id="dom-clobbering-and-mutation-xss">DOM Clobbering and Mutation XSS</h2>

<p>DOM Clobbering and Mutation XSS look like unrelated curiosities. They’re really the same engineering failure seen twice. Security tooling validates one interpretation of a byte stream while the browser executes a different one.</p>

<p>Both attacks exploit a gap between what a security check sees and what the browser does. Here’s how that gap forms, how to test for it, and which defenses help close it.</p>

<h2 id="why-these-bugs-still-happen">Why these bugs still happen</h2>

<p>Cross-site scripting has a well-understood shape: untrusted input reaches somewhere the browser will interpret it as code. A generation of defenses grew up around that model. We escape output, allowlist tags and attributes, ship a Content Security Policy, and assume that if the markup we hand to the page contains no <code class="language-plaintext highlighter-rouge">&lt;script&gt;</code> or event-handler attributes, nothing will run.</p>

<p>DOM Clobbering and Mutation XSS both break that assumption while still satisfying the rules it was built on. Neither requires an injected <code class="language-plaintext highlighter-rouge">&lt;script&gt;</code>. Neither requires an <code class="language-plaintext highlighter-rouge">onerror</code>. Both can pass through sanitizers that a security team would normally consider safe.</p>

<p>The problem is that the check and the execution happen against two different models of the same bytes.</p>

<p>In DOM Clobbering, the two models are about how a name resolves. The sanitizer sees normal elements carrying an <code class="language-plaintext highlighter-rouge">id</code> or <code class="language-plaintext highlighter-rouge">name</code>. The browser sees those same attributes as entries in the named-property namespace of <code class="language-plaintext highlighter-rouge">document</code> and <code class="language-plaintext highlighter-rouge">window</code>. Attacker-controlled markup can then shadow globals that the page’s own JavaScript depends on.</p>

<p>Mutation XSS has the same kind of disagreement, but in parsing. The sanitizer inspects one parse tree and decides that it is clean. That tree is serialized back into a string and inserted again. The browser’s parser runs under a different context and can produce a different tree, potentially one containing executable markup.</p>

<p>The key question throughout is simple: does the browser use the same structure your security check approved?</p>

<h2 id="how-bytes-become-a-live-document">How bytes become a live document</h2>

<p>To understand the gap, it helps to look at what happens between a string of markup and a running page. Three mechanisms matter, and each creates a place where interpretations can diverge.</p>

<h3 id="parsing-is-context-sensitive-and-lossy">Parsing is context-sensitive and lossy</h3>

<p>The HTML parser does not simply map characters to elements one-to-one. It uses a tree-construction algorithm with different modes and insertion rules. What a tag does depends partly on where it appears.</p>

<p>A <code class="language-plaintext highlighter-rouge">&lt;td&gt;</code> outside a table can be dropped. A <code class="language-plaintext highlighter-rouge">&lt;p&gt;</code> can be implicitly closed by a following block element. More importantly for this problem, HTML, SVG, and MathML use different parsing rules. The parser can switch between them in the middle of a document when it crosses into foreign content such as <code class="language-plaintext highlighter-rouge">&lt;svg&gt;</code> or <code class="language-plaintext highlighter-rouge">&lt;math&gt;</code>.</p>

<p>The same characters can therefore represent an element in one context and harmless text in another.</p>

<h3 id="serialization-is-not-the-inverse-of-parsing">Serialization is not the inverse of parsing</h3>

<p>Reading <code class="language-plaintext highlighter-rouge">element.innerHTML</code> serializes the current DOM into a string. Assigning to <code class="language-plaintext highlighter-rouge">innerHTML</code> parses a string into a DOM.</p>

<p>Those operations look like opposites, but they aren’t guaranteed to be.</p>

<p>There are trees whose serialized representation produces a different tree when parsed again. In other words:</p>

<p><code class="language-plaintext highlighter-rouge">parse(serialize(t))</code> does not always equal <code class="language-plaintext highlighter-rouge">t</code>.</p>

<p>That fact is the mechanical basis of Mutation XSS.</p>

<h3 id="names-resolve-against-the-dom-not-just-variables">Names resolve against the DOM, not just variables</h3>

<p>Long before <code class="language-plaintext highlighter-rouge">getElementById</code> existed, browsers allowed scripts to reach elements by name. That behavior is still around.</p>

<p>Elements with certain <code class="language-plaintext highlighter-rouge">id</code> or <code class="language-plaintext highlighter-rouge">name</code> values can participate in named property access on objects such as <code class="language-plaintext highlighter-rouge">window</code> and <code class="language-plaintext highlighter-rouge">document</code>. That means application code which expects a normal JavaScript value may instead resolve an attacker-injected DOM element.</p>

<p>That behavior is the mechanical basis of DOM Clobbering.</p>

<p>These behaviors give the same input more than one meaning. A defense can inspect one meaning while the application acts on another.</p>

<h2 id="dom-clobbering">DOM Clobbering</h2>

<p>DOM Clobbering is basically a client-side code-reuse attack. The attacker does not necessarily inject their own logic. Instead, they inject named markup that changes a value the application’s trusted JavaScript later reads.</p>

<p>The application supplies the useful code. The attacker changes the data that code operates on.</p>

<h3 id="where-the-surface-comes-from">Where the surface comes from</h3>

<p>Named-property behavior is built into browsers. An element with an appropriate <code class="language-plaintext highlighter-rouge">id</code> can become available through named access, while collections of named elements can produce more complicated object-like structures.</p>

<p>Those behaviors give attackers a way to influence values using markup alone.</p>

<h3 id="level-one---shadowing-a-global">Level one - shadowing a global</h3>

<p>If application code guards a feature with a simple truthy or falsy check, an injected named element may be enough to change the result.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Trusted application code already on the page:</span>
<span class="k">if</span> <span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nx">isAdmin</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">renderAdminControls</span><span class="p">();</span>
<span class="p">}</span>

<span class="c1">// Attacker-controlled markup that survives a script-stripping sanitizer:</span>
<span class="c1">// &lt;a id="isAdmin"&gt;&lt;/a&gt;</span>
<span class="c1">//</span>
<span class="c1">// window.isAdmin may now resolve to an element, which is truthy.</span>
</code></pre></div></div>

<p>No attacker JavaScript was injected. The application’s own code made the security-relevant decision.</p>

<h3 id="level-two---producing-a-usable-value">Level two - producing a usable value</h3>

<p>More interesting gadgets read something they expect to be a string, such as a URL that later becomes a script source, redirect, or network destination.</p>

<p>Certain DOM elements, especially anchors, have useful string-like behavior. Combined with named-property resolution, attacker-controlled markup can sometimes supply a value to trusted code.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Trusted gadget expects a configured base URL:</span>
<span class="kd">const</span> <span class="nx">base</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">cfg</span><span class="p">.</span><span class="nx">endpoint</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">s</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">script</span><span class="dl">'</span><span class="p">);</span>

<span class="nx">s</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">base</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">/widget.js</span><span class="dl">'</span><span class="p">;</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">head</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">s</span><span class="p">);</span>

<span class="c1">// Attacker-controlled markup may influence what cfg.endpoint resolves to.</span>
</code></pre></div></div>

<p>The risk is that trusted code reads security-sensitive configuration from a namespace attacker-controlled markup can influence.</p>

<h3 id="level-three---clobbering-built-ins">Level three - clobbering built-ins</h3>

<p>Named access can also interfere with assumptions about properties exposed by browser objects.</p>

<p>Code that assumes a property on <code class="language-plaintext highlighter-rouge">document</code> or <code class="language-plaintext highlighter-rouge">window</code> will always have a particular type can fail when markup changes how that name resolves. Depending on the surrounding code, that may cause anything from a crash to a useful exploitation primitive.</p>

<h3 id="why-it-survives-good-sanitizers">Why it survives good sanitizers</h3>

<p>The attributes DOM Clobbering needs, particularly <code class="language-plaintext highlighter-rouge">id</code> and <code class="language-plaintext highlighter-rouge">name</code>, are also completely normal HTML attributes. Stripping them globally would break legitimate styling, anchors, forms, accessibility behavior, and application logic.</p>

<p>That’s why DOM Clobbering isn’t simply a case of allowing an obviously dangerous tag.</p>

<p>It also has an important precondition: the application needs a <strong>gadget</strong>. There must already be trusted code that reads from a clobberable name and then does something useful with it.</p>

<p>That is what makes clobbering a code-reuse attack. The interesting part isn’t just finding somewhere to inject an <code class="language-plaintext highlighter-rouge">id</code>. It’s finding trusted code that gives that <code class="language-plaintext highlighter-rouge">id</code> power.</p>

<blockquote>
  <p><strong>What makes an application interesting to test</strong></p>

  <ul>
    <li>Security-relevant configuration or feature flags read from <code class="language-plaintext highlighter-rouge">window</code> or <code class="language-plaintext highlighter-rouge">document</code> by name.</li>
    <li>Truthy/falsy or fallback checks that an unexpected DOM element could satisfy.</li>
    <li>HTML injection that permits named elements even though scripts and event handlers are stripped.</li>
  </ul>
</blockquote>

<h2 id="mutation-xss">Mutation XSS</h2>

<p>Mutation XSS takes advantage of the fact that parsing and serialization are not perfect inverses.</p>

<p>A sanitizer receives markup, parses it, decides it is safe, and returns serialized output. Somewhere downstream that output is treated as HTML again and parsed a second time.</p>

<p>If the second parse produces a different tree from the one the sanitizer approved, something that was harmless during validation can become active after insertion.</p>

<p>That’s the mutation in Mutation XSS.</p>

<h3 id="where-the-surface-comes-from-1">Where the surface comes from</h3>

<p>One of the richest sources of parser differences is foreign content.</p>

<p>Inside <code class="language-plaintext highlighter-rouge">&lt;svg&gt;</code> and <code class="language-plaintext highlighter-rouge">&lt;math&gt;</code>, browsers follow different parsing and namespace rules than they do in ordinary HTML. Certain elements also act as transition points where content interpreted one way in one namespace may be interpreted differently after another parse.</p>

<p>This has historically created problems for sanitizers because a sanitizer may reason about one tree while the browser later reconstructs another.</p>

<h3 id="the-mechanism-minimally">The mechanism, minimally</h3>

<p>The basic idea is simple: find markup whose serialization does not parse back into the same structure.</p>

<p>The sanitizer examines a tree where some dangerous-looking fragment is harmless because of its current parsing context. The tree is approved and serialized. When that serialized string is parsed again in another context, the browser produces a different structure.</p>

<p>Conceptually:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- First parse --&gt;</span>
<span class="nt">&lt;svg&gt;</span>
  <span class="nt">&lt;style&gt;content</span> <span class="nt">treated</span> <span class="nt">according</span> <span class="nt">to</span> <span class="nt">this</span> <span class="nt">parsing</span> <span class="nt">context&lt;/style&gt;</span>
<span class="nt">&lt;/svg&gt;</span>

<span class="c">&lt;!-- Sanitizer approves the resulting tree --&gt;</span>

<span class="c">&lt;!-- Tree is serialized back to text --&gt;</span>

<span class="c">&lt;!-- Serialized output is later parsed again --&gt;</span>

<span class="c">&lt;!-- Second parse may not reconstruct the same tree --&gt;</span>
</code></pre></div></div>

<p>The exact payloads change as browsers and sanitizers are patched. That’s part of the point.</p>

<p>If you treat the vulnerability as a list of malicious strings, you’re stuck chasing payloads forever. The underlying problem is the round trip itself.</p>

<p>Researchers have found variations involving namespace confusion, comments and CDATA handling, differences in <code class="language-plaintext highlighter-rouge">&lt;noscript&gt;</code> parsing, and serialization behavior around special characters. The details change. The parser differential remains.</p>

<h3 id="why-sanitize-then-mutate-is-the-failure">Why sanitize-then-mutate is the failure</h3>

<p>The dangerous pattern is structural:</p>

<p><strong>parse → make a trust decision → serialize → parse again → render</strong></p>

<p>The security decision happened during one interpretation. Execution happens after another.</p>

<p>If the second parse can change the markup, test whether that change can make something unsafe run.</p>

<blockquote>
  <p><strong>What makes an application interesting to test</strong></p>

  <ul>
    <li>Sanitizer output returned as a string and later sent to <code class="language-plaintext highlighter-rouge">innerHTML</code>, <code class="language-plaintext highlighter-rouge">outerHTML</code>, or <code class="language-plaintext highlighter-rouge">insertAdjacentHTML</code>.</li>
    <li>Template logic that modifies or reconstructs sanitized markup before insertion.</li>
    <li>Content that crosses HTML, SVG, or MathML parsing boundaries.</li>
  </ul>
</blockquote>

<h2 id="what-the-attacks-have-in-common">What the attacks have in common</h2>

<p>Put DOM Clobbering and Mutation XSS next to each other and the shared problem becomes easier to see.</p>

<p>Both are <strong>differentials</strong>: two parts of the platform claim to understand the same input, but they don’t give it exactly the same meaning. The security decision happens in one model while the final behavior comes from another.</p>

<p><strong>DOM Clobbering</strong> is a differential in the naming model. A sanitizer sees <code class="language-plaintext highlighter-rouge">id</code> and <code class="language-plaintext highlighter-rouge">name</code> as normal attributes. Runtime property resolution may treat them as names available to JavaScript.</p>

<p><strong>Mutation XSS</strong> is a differential in parsing and serialization. The sanitizer approves one tree. The browser later constructs another.</p>

<p>In both cases, the browser gets the last word.</p>

<p>Content Security Policy, output encoding, and allowlist sanitization are still useful defenses. The problem is that none of them automatically removes a disagreement between two interpretations.</p>

<p>A parser differential isn’t necessarily a bad character you forgot to escape or a dangerous tag you forgot to remove. It’s a disagreement about what the input means.</p>

<blockquote>
  <p>The stronger position is architectural: make the component that decides whether something is trusted operate on the same representation that ultimately gets rendered.</p>
</blockquote>

<h2 id="what-to-look-for">What to look for</h2>

<p>Testing looks for a parsing or property-lookup difference that reaches something security-sensitive. Code review asks whether untrusted markup can create that difference in the first place.</p>

<h3 id="finding-an-exploitable-path">Finding an exploitable path</h3>

<ul>
  <li>
    <p><strong>Enumerate the sinks first.</strong> Search client-side code for <code class="language-plaintext highlighter-rouge">innerHTML</code>, <code class="language-plaintext highlighter-rouge">outerHTML</code>, <code class="language-plaintext highlighter-rouge">insertAdjacentHTML</code>, <code class="language-plaintext highlighter-rouge">document.write</code>, and framework equivalents such as <code class="language-plaintext highlighter-rouge">dangerouslySetInnerHTML</code> or <code class="language-plaintext highlighter-rouge">v-html</code>. Sanitized output that later reaches one of these deserves attention.</p>
  </li>
  <li>
    <p><strong>Map gadgets for clobbering.</strong> Look for reads from <code class="language-plaintext highlighter-rouge">window.NAME</code>, <code class="language-plaintext highlighter-rouge">document.NAME</code>, fallback defaults, and configuration pulled from the DOM. Then determine whether attacker-controlled markup can influence the same name.</p>
  </li>
  <li>
    <p><strong>Differential-test the sanitizer.</strong> Parse a candidate fragment, serialize it, parse it again, and compare the resulting DOMs. Structural differences are worth investigating, especially around SVG, MathML, and other known parsing boundaries.</p>
  </li>
  <li>
    <p><strong>Prove impact, not novelty.</strong> A weird parser mutation that reaches nothing is interesting research, but not necessarily an exploitable bug. Trace it to what actually happens: script loading, redirects, authorization behavior, or another security-sensitive sink.</p>
  </li>
</ul>

<h3 id="reviewing-the-code">Reviewing the code</h3>

<ul>
  <li>
    <p><strong>Pay attention to string-typed sanitizer output.</strong> If sanitized HTML becomes a string and later gets parsed again by an HTML sink, review the entire path rather than assuming the sanitizer settles the question.</p>
  </li>
  <li>
    <p><strong>Avoid named global reads for security decisions.</strong> Feature flags, authorization state, endpoint configuration, and similar values should come from controlled application state, not runtime named access on <code class="language-plaintext highlighter-rouge">window</code> or <code class="language-plaintext highlighter-rouge">document</code>.</p>
  </li>
  <li>
    <p><strong>Lint for the round trip.</strong> Static analysis can catch sanitize-to-string-to-HTML-sink patterns before they become a production problem.</p>
  </li>
  <li>
    <p><strong>Watch browser reporting.</strong> CSP and Trusted Types violation reports can expose unexpected sink usage and other behavior worth investigating rather than treating every report as policy noise.</p>
  </li>
</ul>

<h3 id="signals-to-search-for">Signals to search for</h3>

<table>
  <thead>
    <tr>
      <th>Signal</th>
      <th>What it indicates</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">.innerHTML =</code>, <code class="language-plaintext highlighter-rouge">insertAdjacentHTML</code>, <code class="language-plaintext highlighter-rouge">outerHTML</code></td>
      <td>mXSS sink candidates</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">window.NAME</code>, <code class="language-plaintext highlighter-rouge">document.NAME</code>, fallback expressions</td>
      <td>Clobbering gadget candidates</td>
    </tr>
    <tr>
      <td>Security-sensitive reads from DOM-backed names</td>
      <td>Potential clobbering surface</td>
    </tr>
    <tr>
      <td>Sanitizer allowlists permitting <code class="language-plaintext highlighter-rouge">id</code>, <code class="language-plaintext highlighter-rouge">name</code>, forms, and anchors</td>
      <td>Named-property surface worth reviewing</td>
    </tr>
    <tr>
      <td>Round-trip differences across <code class="language-plaintext highlighter-rouge">&lt;svg&gt;</code> and <code class="language-plaintext highlighter-rouge">&lt;math&gt;</code> boundaries</td>
      <td>Mutation candidates</td>
    </tr>
  </tbody>
</table>

<h2 id="defenses">Defenses</h2>

<p>No single control covers both bugs. The practical approach is to avoid reparsing sanitized strings, control access to HTML sinks, and keep trusted state out of names that markup can shadow.</p>

<h3 id="use-native-sanitization-where-it-fits">Use native sanitization where it fits</h3>

<p>The structural problem behind mXSS is the sanitize-serialize-reparse cycle.</p>

<p>Browser-native sanitization APIs can reduce that gap by moving sanitization closer to the parser and the point where content is inserted. Instead of having one library create a trusted string and another parser later reinterpret it, the browser can participate directly in the sanitization and insertion process.</p>

<p>Avoid transformations between the content you approved and the content the browser renders.</p>

<h3 id="trusted-types-closes-the-injection-to-sink-path">Trusted Types closes the injection-to-sink path</h3>

<p>Trusted Types attacks the problem from the sink side.</p>

<p>When enforced, dangerous DOM sinks cannot simply accept arbitrary strings. Code has to pass values through an explicit policy first.</p>

<p>That does not magically decide whether every piece of HTML is safe, but it makes the trust boundary much easier to see and audit. Instead of security depending on every string operation scattered through an application, the decision can be concentrated into a smaller number of controlled policies.</p>

<p>It also turns many accidental “untrusted string reached an HTML sink” cases into browser-enforced failures instead of silent execution.</p>

<h3 id="keep-trusted-state-out-of-clobberable-namespaces">Keep trusted state out of clobberable namespaces</h3>

<p>DOM Clobbering becomes much less useful when important application state does not come from names that markup can influence.</p>

<p>Security-sensitive configuration belongs in module scope, controlled application objects, or other locations whose meaning cannot be changed just because someone inserted an element with a matching <code class="language-plaintext highlighter-rouge">id</code> or <code class="language-plaintext highlighter-rouge">name</code>.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Better: trusted configuration lives in application state,</span>
<span class="c1">// not in a name resolved from attacker-influenced markup.</span>

<span class="k">import</span> <span class="p">{</span> <span class="nx">endpoint</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./config.js</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">s</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">script</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">s</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">endpoint</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">/widget.js</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>

<p>The simplest defense is to avoid this kind of gadget.</p>

<h3 id="gadget-free-libraries-close-the-reach">Gadget-free libraries close the reach</h3>

<p>Clobbering needs trusted code that turns the manipulated value into something useful.</p>

<p>That means libraries should treat named DOM reads as part of their security surface. Configuration, script sources, URLs, and control-flow decisions should not depend on values that attacker-controlled markup can unexpectedly shadow.</p>

<p>This matters especially for widely deployed dependencies. One reusable gadget can create the same attack surface across many applications.</p>

<h3 id="content-security-policy-as-containment-not-cure">Content Security Policy as containment, not cure</h3>

<p>CSP still earns its place as defense in depth.</p>

<p>A strict nonce- or hash-based policy can prevent attacker-controlled script sources or inline code from executing even when another layer fails. But CSP does not remove the underlying parser or resolver differential.</p>

<p>The clobber can still happen. The mutation can still happen. CSP limits what happens afterward.</p>

<p>Use CSP as containment behind those fixes, not as a replacement for them.</p>

<blockquote>
  <p>The defenses that work best all move in the same direction: reduce the distance between the thing you inspect and the thing the browser actually uses. Avoid unnecessary reparsing, control dangerous sinks, and keep trusted state out of namespaces attacker-controlled markup can influence.</p>
</blockquote>

<h2 id="conclusion">Conclusion</h2>

<p>DOM Clobbering and Mutation XSS persist because they arise from browser behavior that individual payload filters cannot reliably remove.</p>

<p>The browser can hold multiple interpretations of the same input. Named-property resolution gives markup meaning that isn’t obvious from looking at the HTML alone. Parsing and serialization can turn one DOM structure into another. Those are old browser behaviors, and trying to enumerate every payload built on top of them is a losing game.</p>

<p>For a security team, the practical takeaways are pretty direct. Treat sanitizer output that later gets parsed again as an mXSS candidate. Treat security-sensitive values resolved through named DOM properties as possible clobbering gadgets. Reduce unnecessary parsing round trips, use browser-enforced controls such as Trusted Types where appropriate, keep trusted configuration out of attacker-influenced namespaces, and use CSP as containment.</p>

<p><strong>Trust the document the browser runs, not only the one your tooling inspected.</strong></p>]]></content><author><name></name></author><category term="web" /><category term="application" /><category term="security" /><summary type="html"><![CDATA[Two Documents, the Same Bytes]]></summary></entry></feed>