Stored XSS That Steals CSRF Tokens for One-Click Account Takeover
SameSite cookies and anti-CSRF tokens block naive cross-site posts. Stored XSS sits inside the origin, so those defenses shrink. Your script can read the CSRF token from a meta tag, hidden input, or JavaScript variable, then fetch a privileged endpoint with the victim’s cookies.
That is why “just XSS” underestimates impact when the app trusts CSRF tokens as its main write protection. Inside the origin, the token is not a secret from JavaScript.
When the chain works
You need:
- Stored XSS that runs in a victim session (admin preview, shared note, support ticket HTML, chat message).
- A sensitive action protected mainly by a CSRF token (email change, password change, OAuth unlink, create API key, add admin).
- Token readable from the DOM or a same-origin endpoint your script can call.
If the password-change API also demands the current password, steal that flow’s step-up or pivot to email-change / session-transfer endpoints instead. API-key minting and OAuth connection endpoints are frequent wins when password change is locked down.
PoC outline
On your attacker account, plant XSS that:
- GETs a page containing the CSRF token (or scrapes
document.querySelector('input[name=csrf']')). - POSTs
/account/email(or equivalent) to an attacker-controlled address. - Optionally exfils the token to your collaborator for debugging — remove exfil in the final video if the email change alone proves ATO.
- Handles double-submit cookie patterns by reading both the cookie-accessible token (if not HttpOnly) and the form field.
Victim (second browser, your second account) views the poisoned content. Show the email changed. Complete takeover via password reset to the new inbox if that is the product’s recovery path. Record timings so triage sees it was one page view, not a multi-step social engineering story.
SameSite and fetch nuances
Same-origin fetch from XSS sends cookies even when SameSite=Lax blocks cross-site POSTs. Custom headers may trigger CORS preflights — prefer form-urlencoded posts the app already uses, mirroring legitimate requests. Match Content-Type and field names exactly; frameworks often reject “almost right” CSRF POSTs.
HttpOnly session cookies stay hidden from JS; you do not need them if CSRF + cookie jar does the write for you. If the CSRF token itself is HttpOnly in a cookie and never appears in the DOM, look for a JSON bootstrap endpoint that returns it to the SPA — XSS can call that too.
Reporting without overclaim
Demo the stored sink, the token read, and the state change on accounts you own. Recommend fixing XSS (encoding/CSP), rotating CSRF tokens after use, binding tokens to session + intent, and requiring re-auth for email/password changes. CSP that blocks inline script raises the bar; it does not replace output encoding on stored HTML fields.
PortSwigger’s CSRF and XSS tracks explain each half. The bounty is the glue: XSS makes CSRF tokens attacker-readable inside the origin. Lead the report with ATO steps, then attach the XSS root cause — triage reads impact first.