| 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
rg "innerHTML|outerHTML|document\.write|\.html\(" --type js --type ts -n src/
rg "eval\(|setTimeout\(|setInterval\(|new Function\(" --type js -n src/
rg "addEventListener.*message" --type js -n src/
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 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
window.addEventListener('message', (e) => {
document.getElementById('output').innerHTML = e.data.html;
});
window.addEventListener('message', (e) => {
if (e.origin !== 'https://trusted.com') return;
});
targetOrigin bypass via IP normalization: When postMessage(data, targetOrigin) uses regex validation like /https?:\/\/[^.]+[.]target[.]com/, the [^.]+ class matches / -- so http://2130706433/.target.com passes the regex. The browser's URL parser then normalizes the integer IP to 127.0.0.1 and sends the message to http://127.0.0.1 (attacker-controlled). Same technique works with hex (0x7f000001) and octal IP forms. Check: does the sender validate targetOrigin with regex rather than strict string equality? If yes, test integer IP + path injection.
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:
{{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
- 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)>
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), custom-sanitizer-audit (homegrown sanitizer), self-xss-escalation (payload only fires in own session)