dom-vulnerability-detection · git:20260528.f3e4348 · 2026-05-28 · sha256 a55fee15eaeb8edb
dom-vulnerability-detection git:20260528.f3e4348A
Immutable. This exact content is served forever at /api/v1/blob/a55fee15eaeb8edb.
---
name: dom-vulnerability-detection
description: DOM-based XSS and client-side vulnerability detection via dynamic analysis — trace attacker-controlled sources to dangerous sinks, audit postMessage handlers, test CSTI in Angular/Vue/htmx, and check for DOM clobbering. Use when auditing JavaScript code, reviewing client-side security, analyzing DOM manipulation, or testing postMessage handlers.
---
## Sources (attacker-controlled input)
`location.hash`, `location.search`, `location.href`, `document.URL`, `document.referrer`, `window.name`, `postMessage event.data`, `document.cookie`, `localStorage`/`sessionStorage`
## Sinks (dangerous output)
`innerHTML`, `outerHTML`, `document.write()`, `eval()`, `setTimeout(string)`, `new Function()`, `location.href=`, `location.assign()`, `element.src=`, `jQuery.html()`, `$.globalEval()`, `v-html`, `ng-bind-html`, `dangerouslySetInnerHTML`
## Analysis workflow
### 1. Find sources reaching sinks
```bash
# Search for dangerous sinks in JS files
grep -rn "innerHTML\|outerHTML\|document\.write\|\.html(" --include="*.js" --include="*.tsx" src/
grep -rn "eval(\|setTimeout(\|setInterval(\|new Function(" --include="*.js" src/
# Search for postMessage handlers without origin checks
grep -rn "addEventListener.*message" --include="*.js" src/
```
If grep finds no sinks, check for dynamically loaded scripts (lazy imports, `document.createElement('script')`) and framework render methods that bypass static grep.
### 2. Trace data flow
For each sink found, trace backwards: does attacker-controlled input reach it?
- Direct: `element.innerHTML = location.hash.slice(1)`
- Via variable: `const data = getParam('q'); ... el.innerHTML = data`
- Via storage: `localStorage.setItem('x', userInput); ... el.innerHTML = localStorage.getItem('x')`
### 3. Check sanitization
If sanitization exists, verify it's adequate:
- DOMPurify → check version, config (is `ALLOW_UNKNOWN_PROTOCOLS` set?)
- Custom sanitizer → see `custom-sanitizer-audit` skill
- Framework auto-escaping → verify not bypassed by `v-html`, `dangerouslySetInnerHTML`, `[innerHTML]`
If sanitization status is ambiguous, attempt bypass patterns from step 6 before marking safe.
### 4. Audit postMessage handlers
```javascript
// VULNERABLE — no origin check
window.addEventListener('message', (e) => {
document.getElementById('output').innerHTML = e.data.html;
});
// SAFE — origin validated
window.addEventListener('message', (e) => {
if (e.origin !== 'https://trusted.com') return;
// ... process e.data
});
```
### 5. Test CSTI (Client-Side Template Injection)
- **AngularJS**: Any user input bound to Angular scope and rendered via `ng-bind-html` or `{{ }}` interpolation enables CSTI. If URL params, hash, or form values flow into scope variables that Angular evaluates, inject `{{constructor.constructor('alert(1)')()}}` as the sandbox escape payload. Check: does `$scope.variable = userInput` exist where `variable` appears in an `ng-bind-html` or `{{ variable }}` template? If yes, it is exploitable. The key insight: AngularJS evaluates template expressions in the scope context, so `searchQuery` from `?q=` reaching `{{searchQuery}}` or `ng-bind-html="searchQuery"` is code execution.
- **Vue.js**: `v-html` directive bypasses Vue's auto-escaping. If user-controlled data (from API, URL, or form) reaches a `v-html` binding, it renders raw HTML. Safe: `{{ variable }}` (auto-escaped). Dangerous: `v-html="variable"`. Check both paths explicitly.
- **htmx**: `hx-get`, `hx-post`, `hx-vals` with user-controlled URLs or values. If URL params populate `hx-get` targets, attacker controls which endpoint htmx fetches from.
### 6. Check browser quirks and library gadgets
- **DOM clobbering**: `<form id="x"><input name="action" value="javascript:alert(1)">` — overwrites `document.x.action`
- **Mutation XSS**: HTML that passes sanitizer but mutates in browser DOM — see `dompurify-mxss-bypass` skill
- **Prototype pollution**: `__proto__` in URL params or JSON reaching `Object.assign`/spread — e.g. `?__proto__[isAdmin]=true` pollutes `Object.prototype` globally when merged via `Object.assign({}, defaults, urlParams)`
- **jQuery .text() entity re-decoding**: `DOMPurify.sanitize(val)` → jQuery `$('<div>'+clean+'</div>').text()` → `innerHTML` = XSS. `.text()` decodes HTML entities that DOMPurify passed through, producing raw tags that innerHTML executes. Also applies to `.val()` and `.textContent` reads.
- **moment.js format injection**: If user input controls `moment().format(input)` and output hits innerHTML, square brackets emit content verbatim: `moment().format("[<img src=x onerror=alert(1)>]")`
- **javascript: URI parsing**: `new URL("javascript://host:443/path%0aalert(1)")` populates `.hostname`, `.port`, `.pathname` — passes allowlist checks. After `javascript:` scheme is stripped, `//` starts a JS single-line comment; `%0a` newline ends it and trailing code executes. Payload: `javascript://ALLOWED_HOST:443/%0aalert(document.domain)`
### 7. Verify exploitability
Build a PoC proving attacker-controlled input triggers the sink:
```
https://target.com/page#<img src=x onerror=alert(document.domain)>
```
Confirm: payload executes, not just reflected. Check CSP — if blocked, see `csp-bypass` skill.
## Chain With
- `csp-bypass` (CSP blocks execution), `dompurify-mxss-bypass` (DOMPurify present), `dom-vulnerability-static-analysis` (grep-based pre-screening), `aem-sling-exploitation` (AEM-specific XSS gadgets)