Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

🏠 Back to Blog

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 read location / document.URL and 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 gonenon-persistent.

Source and sink

ConceptMeaning
SourceWhere untrusted data enters the page in JS—URL (query or fragment), location, form fields, postMessage, storage, etc.
SinkA 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.innerHTML
  • element.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.