XSS → Browser-Confirmed Execution
You suspect XSS somewhere. Reflection is a hint, not proof. Your job is to
make JavaScript actually run in a real browser and capture the proof —
then persist a finding with the exact URL that executed. browser_probe
loads a URL in headless Chrome and reports any alert/confirm/prompt
dialog that fires, even when nothing reflects visibly in the HTML. That
dialog is your oracle.
When this skill applies
- A URL/body/header param value appears in the response body or a header.
- A scanner/audit flagged CWE-79, "reflected XSS", "DOM XSS", or "reflection
observed, execution unconfirmed".
- The page's JS feeds input into a sink:
innerHTML, outerHTML,
document.write, eval, setTimeout(string), location/href,
$(...).html(), dangerouslySetInnerHTML.
If you only have "fuzz everything", anchor on a record first
(query_records for reflected params / search endpoints, then
inspect_record). Don't probe blind.
The canary discipline
Use one unique canary per probe so a dialog can't be a coincidence or a
leftover from another page. Make it specific and greppable, e.g.
alert('xss-9f3a2c'). After browser_probe, the proof is
dialog_fired == true and the dialog message equals your canary. A
dialog with a different message is the app's own JS — not your finding.
remember the canary + target as xss-target so it survives context churn.
Workflow
1. Anchor and find the reflection / sink
inspect_record the candidate. Find where the input lands:
- In the response body → reflected; note the surrounding bytes.
- Consumed by client JS with no server reflection → DOM-based; read the
JS to find the sink and the source (
location.hash, location.search,
document.referrer, postMessage).
- Persisted then rendered on another page (profile, comment, admin
panel) → stored; note the injection request and the rendering URL.
2. Classify the injection context
The payload must break out of its context. Identify which one:
- HTML element body (
<div>HERE</div>): <script>alert('c')</script>
or, if <script> is stripped, <img src=x>.
- HTML attribute (
value="HERE"): close the attribute/tag first —
"><img src=x> or, inside an event-capable tag,
" autofocus tabindex=1 x=".
- JS string literal (
var q='HERE'): close the string —
';alert('c')// or </script><script>alert('c')</script>.
- URL / href sink (
<a href="HERE">, location='HERE'):
javascript:alert('c').
- DOM sink (
el.innerHTML = location.hash.slice(1)): put the payload in
the fragment — #<img src=x> — see step 4.
3. Confirm reflected XSS
Build the full URL with the payload in the query string and call
browser_probe:
url: the endpoint with ?param=<url-encoded payload>.
wait_ms: bump to 1500 if the app defers work (SPA hydration,
setTimeout-wrapped sinks); the default 700 misses late dialogs.
wait_selector: optional CSS selector to wait for before sampling.
Read the result: dialog_fired:true + message == canary → confirmed.
If the value reflects in the HTML but no dialog fires, it's reflected-only
(likely encoded or CSP-blocked) — go to step 6, don't report yet.
4. Confirm DOM-based XSS
DOM XSS often lives in the fragment, which the server never sees (so
server-side reflection and WAFs are blind to it). Put the payload after #:
url: https://target/page#<img src=x> (or
#javascript:alert('c') for a location sink).
browser_probe executes the page's real JS, so a vulnerable
innerHTML = location.hash fires the dialog. No dialog → the sink sanitizes
or the source isn't what you think; re-read the JS.
5. Confirm stored XSS
Two steps:
replay_request the write that persists your payload (post a comment,
update a profile field) with the canary payload in the stored field.
browser_probe the rendering page (where the stored value is shown,
often an admin/other-user view). Dialog fires there → stored XSS, which is
higher impact than reflected (no user interaction / victim is whoever
views the page).
Note the rendering context in the finding — "fires in the admin moderation
queue" is materially worse than "fires on the author's own page".
6. Reflected-but-not-executing: light evasion
If the payload reflects but no dialog fires, the value is encoded, filtered,
or CSP-blocked. Iterate a few targeted variants — don't brute force:
- Case / tag mutation:
<sCRipt>, <svg/onload=alert('c')>,
<img src=x> when script is filtered.
- Encoding: HTML entities, URL double-encoding, or splitting filtered
keywords; match the encoding to where reflection lands.
- Attribute breakout variants if
> or " is stripped but the other
isn't.
- CSP check: if
final_url/headers show a strict Content-Security-Policy
with no unsafe-inline, inline handlers won't run. Note the CSP — it can
downgrade or block the finding; an injection that can't execute under the
deployed CSP is at most informational. Look for a CSP bypass (allowed CDN,
nonce reuse, JSONP endpoint) before claiming execution.
Re-browser_probe after each variant. Stop after a handful; if nothing
fires, report it as reflection-only (low/informational) with the encoding
you observed, not as confirmed XSS.
7. Persist the finding
report_finding once per distinct sink:
severity: stored → high/critical; reflected with execution → high; DOM
with execution → high; reflection-only / CSP-blocked → low or
informational.
title: type + endpoint + param/sink, e.g. "Reflected XSS in /search?q executes arbitrary JS" or "DOM XSS via location.hash → innerHTML on /dashboard".
cwe_id: CWE-79.
description: 2–3 sentences — context, the payload, that a real browser
fired alert(<canary>), and the rendering context for stored XSS.
- Include the exact URL (or the write request + rendering URL for stored)
that fired the dialog — this is the reproducible proof.
If the audit harness already filed a theoretical XSS for the same sink,
update_finding with status: triaged and the browser-confirmed evidence
instead of double-reporting.
Pitfalls — read before claiming
- Reflection ≠ execution. Never report XSS off a string match alone.
The dialog from
browser_probe is the bar.
- Match the canary. A fired dialog whose message isn't your canary is the
app's own code; keep the canary unique per probe.
- Deferred alerts need a larger
wait_ms — a sink wrapped in
setTimeout/requestAnimationFrame won't fire within the default window.
- Fragments aren't sent to the server. For DOM/hash XSS, the WAF and
server logs won't show it — that's expected; the browser still executes it.
- CSP can make a "working" payload inert in production. Always check
whether inline execution is actually allowed before sizing the finding.
- One
report_finding per sink, not per payload variant tried.
Output expectations
- One finding per confirmed sink, each with a browser-fired-dialog proof URL.
- A
remember note with the canonical confirming URL + canary.
- The plan item that triggered this skill marked
done via update_plan.
1---2name: xss-browser-confirm3description: Turn a suspected Cross-Site Scripting reflection or DOM sink into proof of JavaScript execution by firing a uniquely-tagged dialog in a real headless browser via the browser_probe tool — not by string-matching the response. Covers reflected, stored, and DOM-based XSS, context-aware payload crafting (HTML body, attribute, JS string, URL/href, DOM sink), light WAF/encoding evasion when a payload reflects but doesn't execute, and persisting a finding sized by real impact. Use when a parameter's value appears in the response, when a DOM sink (innerHTML, document.write, eval, location) consumes input, when CWE-79 was flagged, or when a scanner saw reflection but couldn't confirm execution.4license: MIT5---67# XSS → Browser-Confirmed Execution89You suspect XSS somewhere. Reflection is a hint, not proof. Your job is to10make JavaScript actually *run* in a real browser and capture the proof —11then persist a finding with the exact URL that executed. `browser_probe`12loads a URL in headless Chrome and reports any `alert`/`confirm`/`prompt`13dialog that fires, even when nothing reflects visibly in the HTML. That14dialog is your oracle.1516## When this skill applies1718- A URL/body/header param value appears in the response body or a header.19- A scanner/audit flagged CWE-79, "reflected XSS", "DOM XSS", or "reflection20 observed, execution unconfirmed".21- The page's JS feeds input into a sink: `innerHTML`, `outerHTML`,22 `document.write`, `eval`, `setTimeout(string)`, `location`/`href`,23 `$(...).html()`, `dangerouslySetInnerHTML`.2425If you only have "fuzz everything", anchor on a record first26(`query_records` for reflected params / search endpoints, then27`inspect_record`). Don't probe blind.2829## The canary discipline3031Use one **unique** canary per probe so a dialog can't be a coincidence or a32leftover from another page. Make it specific and greppable, e.g.33`alert('xss-9f3a2c')`. After `browser_probe`, the proof is34`dialog_fired == true` **and** the dialog message equals your canary. A35dialog with a different message is the app's own JS — not your finding.3637`remember` the canary + target as `xss-target` so it survives context churn.3839## Workflow4041### 1. Anchor and find the reflection / sink4243`inspect_record` the candidate. Find **where** the input lands:4445- In the **response body** → reflected; note the surrounding bytes.46- Consumed by **client JS** with no server reflection → DOM-based; read the47 JS to find the sink and the source (`location.hash`, `location.search`,48 `document.referrer`, `postMessage`).49- Persisted then rendered on **another page** (profile, comment, admin50 panel) → stored; note the injection request and the rendering URL.5152### 2. Classify the injection context5354The payload must break out of *its* context. Identify which one:5556- **HTML element body** (`<div>HERE</div>`): `<script>alert('c')</script>`57 or, if `<script>` is stripped, `<img src=x onerror=alert('c')>`.58- **HTML attribute** (`value="HERE"`): close the attribute/tag first —59 `"><img src=x onerror=alert('c')>` or, inside an event-capable tag,60 `" onmouseover=alert('c') autofocus tabindex=1 x="`.61- **JS string literal** (`var q='HERE'`): close the string —62 `';alert('c')//` or `</script><script>alert('c')</script>`.63- **URL / href sink** (`<a href="HERE">`, `location='HERE'`):64 `javascript:alert('c')`.65- **DOM sink** (`el.innerHTML = location.hash.slice(1)`): put the payload in66 the **fragment** — `#<img src=x onerror=alert('c')>` — see step 4.6768### 3. Confirm reflected XSS6970Build the full URL with the payload in the query string and call71`browser_probe`:7273- `url`: the endpoint with `?param=<url-encoded payload>`.74- `wait_ms`: bump to `1500` if the app defers work (SPA hydration,75 `setTimeout`-wrapped sinks); the default `700` misses late dialogs.76- `wait_selector`: optional CSS selector to wait for before sampling.7778Read the result: `dialog_fired:true` + message == canary → **confirmed**.79If the value reflects in the HTML but no dialog fires, it's reflected-only80(likely encoded or CSP-blocked) — go to step 6, don't report yet.8182### 4. Confirm DOM-based XSS8384DOM XSS often lives in the **fragment**, which the server never sees (so85server-side reflection and WAFs are blind to it). Put the payload after `#`:8687- `url`: `https://target/page#<img src=x onerror=alert('c')>` (or88 `#javascript:alert('c')` for a location sink).89- `browser_probe` executes the page's real JS, so a vulnerable90 `innerHTML = location.hash` fires the dialog. No dialog → the sink sanitizes91 or the source isn't what you think; re-read the JS.9293### 5. Confirm stored XSS9495Two steps:96971. `replay_request` the write that persists your payload (post a comment,98 update a profile field) with the canary payload in the stored field.992. `browser_probe` the **rendering** page (where the stored value is shown,100 often an admin/other-user view). Dialog fires there → stored XSS, which is101 higher impact than reflected (no user interaction / victim is whoever102 views the page).103104Note the rendering context in the finding — "fires in the admin moderation105queue" is materially worse than "fires on the author's own page".106107### 6. Reflected-but-not-executing: light evasion108109If the payload reflects but no dialog fires, the value is encoded, filtered,110or CSP-blocked. Iterate a *few* targeted variants — don't brute force:111112- **Case / tag mutation**: `<sCRipt>`, `<svg/onload=alert('c')>`,113 `<img src=x onerror=alert('c')>` when `script` is filtered.114- **Encoding**: HTML entities, URL double-encoding, or splitting filtered115 keywords; match the encoding to where reflection lands.116- **Attribute breakout** variants if `>` or `"` is stripped but the other117 isn't.118- **CSP check**: if `final_url`/headers show a strict `Content-Security-Policy`119 with no `unsafe-inline`, inline handlers won't run. Note the CSP — it can120 downgrade or block the finding; an injection that can't execute under the121 deployed CSP is at most informational. Look for a CSP bypass (allowed CDN,122 `nonce` reuse, JSONP endpoint) before claiming execution.123124Re-`browser_probe` after each variant. Stop after a handful; if nothing125fires, report it as reflection-only (low/informational) with the encoding126you observed, not as confirmed XSS.127128### 7. Persist the finding129130`report_finding` once per distinct sink:131132- `severity`: stored → high/critical; reflected with execution → high; DOM133 with execution → high; reflection-only / CSP-blocked → low or134 informational.135- `title`: type + endpoint + param/sink, e.g. `"Reflected XSS in136 /search?q executes arbitrary JS"` or `"DOM XSS via location.hash →137 innerHTML on /dashboard"`.138- `cwe_id`: CWE-79.139- `description`: 2–3 sentences — context, the payload, that a real browser140 fired `alert(<canary>)`, and the rendering context for stored XSS.141- Include the **exact URL** (or the write request + rendering URL for stored)142 that fired the dialog — this is the reproducible proof.143144If the audit harness already filed a theoretical XSS for the same sink,145`update_finding` with `status: triaged` and the browser-confirmed evidence146instead of double-reporting.147148## Pitfalls — read before claiming149150- **Reflection ≠ execution.** Never report XSS off a string match alone.151 The dialog from `browser_probe` is the bar.152- **Match the canary.** A fired dialog whose message isn't your canary is the153 app's own code; keep the canary unique per probe.154- **Deferred alerts** need a larger `wait_ms` — a sink wrapped in155 `setTimeout`/`requestAnimationFrame` won't fire within the default window.156- **Fragments aren't sent to the server.** For DOM/hash XSS, the WAF and157 server logs won't show it — that's expected; the browser still executes it.158- **CSP can make a "working" payload inert** in production. Always check159 whether inline execution is actually allowed before sizing the finding.160- One `report_finding` per sink, not per payload variant tried.161162## Output expectations163164- One finding per confirmed sink, each with a browser-fired-dialog proof URL.165- A `remember` note with the canonical confirming URL + canary.166- The plan item that triggered this skill marked `done` via `update_plan`.