Self-XSS Alone Is Low — Chain It With CSRF for Real ATO Impact
Self-XSS gets closed as “requires the victim to paste JS into their own console.” Fair. Programs are tired of that report. The bug becomes interesting the moment someone else can plant that payload without the victim typing it.
CSRF is the usual glue. You force the logged-in victim to submit a profile field, note, display name, or webhook URL that later renders as HTML. Their browser stores your XSS. Your XSS then runs in their session.
What “self” actually means here
Classic Self-XSS: reflection only when the attacker enters the input in their own browser. No other user ever sees it.
Chained Self-XSS: a state-changing request (often CSRF-able) writes attacker-controlled markup into a field the victim will view. The XSS is no longer self-inflicted.
I’ve seen triage flip from Informative to High after one extra request in the PoC. The XSS did not change. The delivery path did.
Hunt for plantable sinks
Look for authenticated POSTs that update:
- Bio, signature, custom CSS, email signature
- Saved searches, dashboard widgets, “about” HTML
- Support ticket drafts that echo back to the same user
- Preferences that render in an admin “preview as user” pane
- Chat “saved replies” or canned responses that expand as HTML
Map which of those lack CSRF tokens, use cookie-only auth, or accept Content-Type: text/plain / form posts that bypass SPA JSON protections. SPAs often send JSON with a custom header; try a plain HTML form POST to the same path. If the server still accepts it, CSRF is back on the table.
Also check IDOR: can user B write into user A’s “private” field? That skips CSRF entirely and still turns Self-XSS into stored XSS for someone else.
Proof workflow (two accounts)
- Account A finds a Self-XSS sink (payload executes when A views their own page).
- Account B (attacker) crafts a CSRF page that submits A’s update endpoint with the XSS payload.
- Log in as A in another browser, open B’s CSRF page, then visit the page that renders the field.
- Show session cookie theft, email change, or OAuth reconnect — something that proves ATO, not just
alert(1).
If SameSite cookies block classic CSRF, try top-level navigations that still mutate state, or login CSRF into a flow that then auto-writes the field. Document cookie flags honestly. Lax cookies still send on top-level GET navigations; some “update preference” endpoints wrongly accept GET.
Impact language that survives triage
Lead with the chain: “Unauthenticated attacker forces victim to store XSS → XSS runs in victim session → change password / email / API key.” Attach the CSRF HTML, the resulting stored value, and the privileged action your payload triggered.
Do not claim Self-XSS severity for console paste. Do claim chained impact when delivery is one click (or zero click) for a normal logged-in user. If the victim must already be on an obscure settings page, say so — honesty keeps trust with the program.
Fixes to recommend
- CSRF tokens (or SameSite=Strict where it fits) on every state change
- Output encoding / CSP that blocks inline script on profile surfaces
- Treat “user-only” HTML fields as untrusted — they become shared the day a CSRF or IDOR writes them
- Reject non-JSON content types on APIs that the SPA never uses as forms
PortSwigger’s Self-XSS notes and CSRF labs are the right mental models. Pair them on purpose.