Safeguard
Vulnerabilities

DOM-Based XSS: Finding and Fixing Client-Side Injection

DOM XSS never touches your server, so response scanners miss it. Here is how to trace sources to sinks in client code and shut the flaw down.

Yukti Singhal
Head of Product
5 min read

DOM XSS is a cross-site scripting flaw that lives entirely in client-side JavaScript: the page reads attacker-controlled data from a source like the URL fragment and writes it into a dangerous sink like innerHTML, so the malicious script executes without the server ever seeing the payload. That last part is what makes it slippery. The vulnerable code and the exploit both run in the browser, so server logs, response scanners, and most WAFs never observe the attack.

If reflected and stored XSS are server-side rendering bugs, DOM XSS is a data-flow bug in your own JavaScript.

How is DOM XSS different from reflected XSS?

In reflected XSS, the server takes input from the request, places it into the HTML response unencoded, and sends it back. The proof is visible in the response body.

In DOM XSS, the server may return a completely static, safe page. The vulnerability appears only when client-side code runs: it pulls data from a source, then hands it to a sink that parses HTML or evaluates code. Because the payload can live in the URL fragment (the part after #), browsers never even send it to the server. A request for a page with a fragment payload looks, from the server's side, like a request for the clean page.

That difference has a direct consequence: you cannot find DOM XSS by grepping HTTP responses. You have to analyze the JavaScript.

What are the sources and sinks that cause DOM XSS?

DOM XSS is a source-to-sink problem. A source is any place the page reads attacker-influenced data. A sink is any API that turns a string into markup or executable code. The bug is a path connecting the two without sanitization.

Common sources:

  • location.hash, location.search, location.href, and document.URL
  • document.referrer
  • window.name
  • postMessage data
  • values read back from localStorage or sessionStorage

Dangerous sinks:

  • element.innerHTML, element.outerHTML, insertAdjacentHTML
  • document.write and document.writeln
  • eval, Function(), setTimeout/setInterval with string arguments
  • assigning to location or location.href with a javascript: URL
  • jQuery's .html() and, in older versions, $() selector parsing

A concrete example: code that reads location.hash, strips the leading #, and assigns it to a container's innerHTML is directly exploitable. A visit to a URL whose fragment contains <img src=x onerror=alert(1)> runs the script, and the server logs show nothing but a normal page load.

How do you find DOM XSS in a codebase?

Finding DOM XSS means following data, not scanning pages.

  1. Enumerate the sinks first. Search the client bundle and source for innerHTML, document.write, eval, insertAdjacentHTML, .html(, and assignments to location. Each hit is a candidate.
  2. Walk backward to a source. For every sink, trace whether its argument can originate from location, postMessage, storage, or another attacker-influenced input. If yes, and there is no sanitizer on the path, you have a finding.
  3. Test dynamically. Load the page with a marker in the fragment, the query, and window.name, then watch whether it reaches the DOM as live markup. Browser devtools breakpoints on the sink function make the path obvious.
  4. Automate the taint trace. Manual review does not scale past a few thousand lines of minified JavaScript.

Purely dynamic scanners struggle here because triggering the sink often requires exercising a specific client-side state. Static analysis that models JavaScript data flow can flag the source-to-sink path directly. Safeguard's SAST and DAST pair a taint-tracking static engine with a dynamic crawler so DOM paths that only one of them would catch are covered by the other. For the underlying mechanics of each XSS variant, the Safeguard Academy has a client-side security track.

How do you fix and prevent DOM-based XSS?

The fix is the same principle as all XSS, applied on the client: keep untrusted data out of code and markup contexts.

  • Prefer safe sinks. Use textContent or innerText instead of innerHTML whenever you are inserting plain text. This alone eliminates most DOM XSS.
  • Sanitize required HTML. When you genuinely need to render user HTML, run it through DOMPurify before assignment. Do not hand-roll a filter.
  • Adopt Trusted Types. The browser Trusted Types API can enforce that dangerous sinks only accept vetted values, turning a whole class of bugs into a runtime error at the sink. Pair it with a Content Security Policy that requires it.
  • Validate URL schemes. Before assigning user data to location or an href, allowlist http, https, and mailto, and reject javascript:.
  • Treat postMessage as untrusted. Verify event.origin and validate the message shape before using the data.

A strict CSP that blocks inline script and eval is strong defense in depth, but it is not a substitute for fixing the source-to-sink path. Some DOM XSS variants (for example, injecting into an existing allowed script source) can survive a CSP.

FAQ

Why do vulnerability scanners miss DOM XSS?

Because many scanners inspect HTTP responses, and DOM XSS lives in client-side execution. When the payload rides in the URL fragment, it is never sent to the server at all, so nothing in the request or response reveals it. Detecting it requires analyzing JavaScript data flow or driving the running page.

Is DOM XSS more or less dangerous than stored XSS?

The impact per victim is the same: arbitrary script in an authenticated session. Stored XSS usually reaches more victims because it is served to everyone who loads a record, while DOM XSS often requires luring a victim to a crafted URL. Severity depends on delivery reach, not the variant name.

Does using React or Angular prevent DOM XSS?

They prevent the common cases by auto-escaping and discouraging raw HTML insertion, but escape hatches like dangerouslySetInnerHTML, bypassSecurityTrustHtml, and direct DOM manipulation reopen the risk. Framework use lowers DOM XSS; it does not remove it.

What single change reduces DOM XSS the most?

Replacing innerHTML with textContent wherever you are inserting text rather than markup. It removes the sink entirely for the most common vulnerable pattern.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.