DOM-Based XSS
What it is
DOM-based XSS is the third main XSS type. Like reflected XSS, it is usually non-persistent: the dangerous output does not survive a simple revisit without the same client-side inputs (e.g. fragment or client state).
The difference from reflected XSS: data is not necessarily sent to the back end in an HTTP request that returns your string in HTML. Instead, JavaScript on the client reads untrusted input and writes it into the DOM. The server may serve a static page; the vulnerability is in client-side source → sink handling.
Lab clues (to-do style app)
Behavior that suggests DOM XSS:
- Network tab: Adding an item triggers no new HTTP request—only client-side updates.
- URL uses a hash fragment (e.g.
#task=...). The fragment is not sent to the server in normal navigation; the browser keeps it and client script can readlocation/document.URLand parse parameters after#. - View page source (
Ctrl+U): Your test string may not appear. The base HTML was fetched before JS ran; the DOM is updated after load when you click Add. Web Inspector (Ctrl+Shift+C) shows the live DOM including injected text. - Refresh without the same fragment/state: the injected content is gone → non-persistent.
Source and sink
| Concept | Meaning |
|---|---|
| Source | Where untrusted data enters the page in JS—URL (query or fragment), location, form fields, postMessage, storage, etc. |
| Sink | A function or API that writes that data into the DOM (or otherwise executes it). If the sink does not sanitize or encode for the HTML context, DOM XSS is possible. |
Common sinks (examples):
document.write()element.innerHTMLelement.outerHTML
jQuery examples that can write HTML:
.add(),.after(),.append()
If user-controlled data reaches a sink without safe encoding and without a safer API (e.g. textContent for plain text), treat the page as a DOM XSS candidate.
Example vulnerable pattern (illustrative)
Reading the task from the URL (fragment or query—lab used task= in the URL string the script parses):
var pos = document.URL.indexOf("task=");
var task = document.URL.substring(pos + 5, document.URL.length);
Writing it into the DOM with innerHTML (no sanitization):
document.getElementById("todo").innerHTML = "<b>Next Task:</b> " + decodeURIComponent(task);
Attacker controls task; the string is concatenated into HTML → XSS if a working payload is injected.
Why <script> often fails in innerHTML
Many browsers do not execute <script> nodes inserted via innerHTML as a mitigation. That does not mean the page is safe: use payloads that do not rely on <script>, for example an image with a bad src and onerror:
<img src="" onerror=alert(window.origin)>
Encoded for a URL fragment (example shape):
http://SERVER_IP:PORT/#task=<img src='' onerror=alert(window.origin)>
(Exact encoding depends on context; decodeURIComponent in the sink affects how you must encode the fragment.)
Delivering DOM XSS to a victim
If the source is in the URL (especially the fragment), share the full URL including #task=.... When the victim loads it, client-side script reads the source and hits the unsafe sink—no server round-trip required for the malicious string to reach the DOM.
Payload choice varies with filters, context, and browser behavior; innerHTML blocking <script> is one common reason to pivot to event-handler or other tags.