Injection
Log in Join

Lesson 7 · Reading · 30 min

DOM-based XSS

There is a kind of XSS you will never find by reading the server's response, because the server never sees the payload. The vulnerability lives entirely in the page's own JavaScript: it reads something you control and writes it into the page in a dangerous way, all in the browser. This is DOM-based XSS, and finding it is a different skill, you read the JavaScript, not the HTML.

Why it is different

In reflected and stored XSS, your input goes to the server and comes back in the response, so you can see it by viewing source. In DOM-based XSS, client-side JavaScript takes input from a source: a place the URL or environment provides, and passes it to a sink: a function that turns text into live page content or code, without safely handling it in between. The dangerous data flow happens after the page has loaded, so the server's response looks completely clean. If you only test by reading responses, you miss it entirely.

Sources and sinks: the two things to look for

Sources are where attacker-influenced data enters the JavaScript:

  • location, location.href, location.search (the ?query), and especially location.hash (the #fragment: note the fragment is never even sent to the server, so it is invisible server-side).
  • document.referrer, document.cookie, window.name.
  • postMessage data from other windows.

Sinks are where data becomes executable or becomes markup:

  • element.innerHTML, outerHTML, document.write(): text becomes HTML (and can carry a script-running tag).
  • eval(), setTimeout(string), Function(): text becomes code.
  • location = ..., element.src = ... with a javascript: URL.
  • Framework equivalents like jQuery's $(...).html().

DOM XSS exists when a source flows to a sink with no safe handling in between. Your job is to find that flow.

How to hunt it

You read the client-side code:

  1. Open the JavaScript (DevTools → Sources) and search it for the sink names above: innerHTML, document.write, eval, location. Each hit is a candidate.
  2. Trace backwards from the sink: where does the value it writes come from? If it traces back to location.hash or location.search or another source, you may have a flow.
  3. Test the flow by putting a marker in the source and watching whether it reaches the sink. For a hash sink, try #zqxmarker and see if it appears in the DOM.
  4. Escalate to a payload shaped for that sink. For an innerHTML sink fed from the hash:
https://target/page#<img src=x onerror=alert(document.domain)>

The page's JavaScript reads location.hash, writes it into innerHTML, the browser parses your <img>, the onerror fires. Note the payload was in the #fragment, which never left the browser, a server-side test would show nothing.

Browser DevTools can help: modern DevTools and extensions can flag some source-to-sink flows, and Burp's DOM Invader is built specifically to trace them automatically. But understanding sources and sinks by hand is what lets you recognise and confirm the flow.

Why filters and encoding differ here

Server-side output encoding does not help DOM XSS, because the dangerous step happens in the browser after the response. The fix lives in the JavaScript: use safe sinks (textContent instead of innerHTML), validate the source, and frameworks that auto-escape. This also means a site with perfect server-side encoding can still be fully vulnerable to DOM XSS, which is exactly why it is worth checking separately even when reflected/stored testing came up clean.

Filter bypass in the DOM

DOM XSS endpoints often have client-side filters that strip or block certain characters before the value hits the sink. Because it is all in JavaScript you can read, you can see how the filter works and craft input it misses: alternate tags and handlers, encoding the browser will decode before the sink parses it, or exploiting a sink that decodes its input. A filtered DOM sink is not a safe one; it is one whose filter you have not yet read carefully enough.

The honesty gate

Same as the other XSS lessons: prove execution with a benign payload showing your script ran on the origin, demonstrate the delivery (the crafted URL, including the #fragment if that is the source), and test against your own browser rather than real users. Show that the source-to-sink flow executes; that is the finding.

Your lab

Lumen Realty DOM XSS filter bypass puts both skills together: a genuine DOM-based flow (a source reaching a sink in the page's JavaScript) guarded by a client-side filter you must get past. Read the JavaScript to find the source and sink, work out what the filter blocks, and craft a payload that survives the filter and fires in the sink. When your alert runs, you have found a bug that is invisible in the server's response, the mark of understanding DOM XSS.

Where this goes next

That completes cross-site scripting: reflected, stored, and DOM-based. The last lesson of this path zooms out to a very practical, exam-relevant skill: taking a content-management system you fingerprinted in recon and turning its known weaknesses into targeted findings.

This lesson has 2 labs to try what you just read.