How does DOM-based XSS differ from server-side XSS?
The server never puts the payload into its response — client-side JavaScript reads it out of the URL and writes it into the page, so server-side filters have nothing to catch. With a # fragment the payload doesn't even leave the browser.
In server-side XSS the vulnerable line is on the server, which echoes untrusted data into the HTML it sends. In DOM-based XSS the vulnerable line is in the page's own JavaScript: a source reads attacker-controlled data, a sink writes it into the document.
A page that pre-selects a language from the query string:
document.write("<OPTION value=1>"+document.location.href.substring(
document.location.href.indexOf("default=")+8)+"</OPTION>");
Here the source is document.location.href and the sink is document.write — which parses whatever it is handed as HTML. Nothing validates the slice in between.
Attack URL:
http://www.some.site/page.html?default=<script>alert(document.cookie)</script>
What the server sees — and this is the part people get wrong:
- With
?default=…, the payload is sent to the server as a query parameter. But the server never reflects it, so the response is unchanged and a server-side output filter has nothing to inspect. The attack happens after the response arrives. - The attacker can go further and move the payload into the fragment:
http://www.some.site/page.html#default=…. Browsers never transmit the part after#, so now the payload genuinely never reaches the server, never appears in access logs, and is invisible to a WAF.
Why that matters for defence: server-side controls — request filtering, output encoding in the template, logging — are the wrong layer here. The fix has to live in the client code: read from a safe source, and write with a text-only sink (textContent) instead of an HTML-parsing one (innerHTML, document.write), with a Content Security Policy as the backstop.
Go deeper:
PortSwigger — DOM-based XSS — sources and sinks; why the server never sees the payload.