dom-vulnerability-detection · diff
git:20260528.f3e4348 to git:20260529.8f6ea0e
23 added, 24 removed. Audit A to A.
---
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.
+ 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/
+ rg "innerHTML|outerHTML|document\.write|\.html\(" --type js --type ts -n src/
+ rg "eval\(|setTimeout\(|setInterval\(|new Function\(" --type js -n src/
# Search for postMessage handlers without origin checks
- grep -rn "addEventListener.*message" --include="*.js" src/
+ rg "addEventListener.*message" --type js -n 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')`
+ **Checkpoint:** For each sink, document: source -> transformations -> sink. If no attacker-controlled source reaches the sink, mark as not exploitable and move on.
+
### 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.
+ If sanitization exists, verify it is 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]`
### 4. Audit postMessage handlers
```javascript
- // VULNERABLE — no origin check
+ // VULNERABLE -- no origin check
window.addEventListener('message', (e) => {
document.getElementById('output').innerHTML = e.data.html;
});
- // SAFE — origin validated
+ // SAFE -- origin validated
window.addEventListener('message', (e) => {
if (e.origin !== 'https://trusted.com') return;
// ... process e.data
});
```
+ **Checkpoint:** For each handler, verify: (1) strict `e.origin` equality check exists, (2) no `window.origin` comparison, (3) no `startsWith`/`endsWith` on origin, (4) data is not passed to dynamic execution (`window[data.func]`).
+
### 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.
+ - **AngularJS**: `{{constructor.constructor('alert(1)')()}}`
+ - **Vue.js**: check if user input reaches `v-html` or template interpolation
+ - **htmx**: `hx-get`, `hx-post` with user-controlled URLs
- ### 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)`
+ ### 6. Check browser quirks
+ - **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
### 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.
+ **Checkpoint:** 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)
+ - `csp-bypass` (CSP blocks execution), `dompurify-mxss-bypass` (DOMPurify present), `custom-sanitizer-audit` (homegrown sanitizer), `self-xss-escalation` (payload only fires in own session)