TL;DR: The alert(1) proof-of-concept gives the impression that XSS is a nuisance — a popup box that annoys users. In reality, cross-site scripting is the most reliable path to full account takeover in web applications. An XSS payload can hijack sessions, exfiltrate sensitive data, create persistent backdoor accounts, keylog credentials, and pivot into internal systems. Despite modern framework protections and Content Security Policies, XSS remains the most commonly found vulnerability in our web application penetration tests — because it has evolved faster than the defenses designed to prevent it.
Why XSS Refuses to Die
Every year, the security industry declares XSS a solved problem. React auto-escapes. Angular sanitizes. CSP blocks inline scripts. And every year, XSS remains the #1 or #2 most common finding in web application penetration tests.
The reason is that XSS is not a single vulnerability — it is a category of vulnerabilities that exists wherever user-controlled data meets a rendering context. Modern frameworks handle the default case well: rendering user text inside an HTML element. But applications are complex. They render user data in HTML attributes, JavaScript strings, CSS values, URLs, SVG elements, Markdown, PDF generators, email templates, and WebSocket messages. Each context has different escaping requirements, and a single context where the framework's default protection does not apply creates an exploitable XSS vector.
At Lorikeet Security, we find XSS in the majority of our web application assessments. The nature of the findings has shifted — from simple reflected XSS in server-rendered pages to DOM XSS in single-page applications, mutation XSS that bypasses sanitizers, and stored XSS through file uploads and third-party integrations. The attack surface has changed, but the fundamental problem — rendering untrusted data in a trusted context — has not.
Real Exploitation: What Happens After alert(1)
The alert(1) proof-of-concept exists because it is the simplest way to demonstrate script execution. But the actual exploitation of XSS in a penetration test or real attack looks nothing like a popup box:
Session Hijacking
If session tokens are stored in cookies without the httpOnly flag, a single line of JavaScript exfiltrates them: the attacker's script reads document.cookie and sends the value to an attacker-controlled server. With the session token, the attacker authenticates as the victim from any browser, anywhere in the world. Even with httpOnly cookies, the attacker can make authenticated API requests from within the victim's browser session — changing email addresses, creating API keys, or exfiltrating data through the application's own endpoints.
Credential Harvesting
An XSS payload can inject a fake login overlay that exactly matches the application's styling, displaying "Your session has expired. Please log in again." When the victim enters their credentials, they are sent to the attacker. The overlay disappears, the user sees the normal application, and they have no indication their credentials were stolen. More sophisticated payloads install keyloggers that capture every keystroke on the page — including passwords typed into legitimate form fields.
Account Takeover Chain
The most reliable XSS exploitation chain for account takeover: the script uses the victim's authenticated session to call the "change email" API endpoint, updating the account email to an attacker-controlled address. Then the attacker triggers a password reset to their new email. The victim's password is reset, the attacker sets a new one, and they have permanent access. No cookies needed, no session tokens stolen — just API calls made with the victim's active session.
The Three Types of XSS
Reflected XSS
The payload is included in a request (usually a URL parameter) and reflected in the response without sanitization. The attacker crafts a malicious URL and tricks the victim into clicking it. The server includes the payload in the rendered page, and the victim's browser executes it. Reflected XSS requires social engineering to deliver the payload but remains common in search functions, error pages, and any feature that reflects user input in the response.
Stored XSS
The payload is stored server-side (in a database, file, or cache) and served to other users who view the affected page. Common storage points include user profiles, comments, forum posts, support tickets, file names, and metadata fields. Stored XSS is more dangerous than reflected because it does not require social engineering — every user who views the affected page is compromised. A stored XSS in an admin-visible field (like a support ticket or user registration) directly targets the highest-privilege accounts in the application.
DOM-Based XSS
The payload never reaches the server. Client-side JavaScript reads data from an attacker-controlled source — location.hash, location.search, document.referrer, postMessage, localStorage — and writes it to a dangerous sink like innerHTML, document.write, eval(), or location.href. DOM XSS is exploding in single-page applications because client-side routing, dynamic content rendering, and inter-component communication create dozens of source-to-sink data flows that are invisible to server-side security controls.
Modern XSS Vectors
Mutation XSS (mXSS)
Mutation XSS exploits the difference between how an HTML sanitizer parses input and how the browser actually renders it. The sanitizer sees safe HTML. But when the browser processes it, DOM mutations (element rebalancing, attribute parsing differences, encoding normalization) transform the safe HTML into an executable payload. mXSS has historically bypassed DOMPurify, Google Closure Library, and browser-native sanitizers because the mutation behavior is a feature of the HTML specification, not a bug.
Template Injection in Frontend Frameworks
When user input is interpolated into Angular templates ({{ }} expressions), Vue templates, or other client-side template engines, the template engine may evaluate the input as code. Angular's template syntax allows arbitrary JavaScript execution through constructor chains. If user input reaches a template compilation context — often through server-side rendering that injects user data into the Angular template — the result is client-side code execution.
Prototype Pollution to XSS
Prototype pollution vulnerabilities in JavaScript libraries (lodash merge, jQuery extend, recursive object merge functions) allow attackers to inject properties into Object.prototype. If a rendering library or framework checks for the existence of a property without using hasOwnProperty, the polluted prototype value is used — potentially injecting attacker-controlled HTML attributes, event handlers, or script sources into rendered elements.
XSS Through File Uploads
SVG files can contain JavaScript in <script> tags or event handler attributes. HTML files uploaded as "documents" execute in the application's origin. Even image files can be crafted with polyglot payloads that execute as JavaScript when served with an incorrect MIME type. If uploaded files are served from the same origin as the application — rather than a separate, cookieless domain — any file-based XSS has full access to the victim's authenticated session.
CSP Bypass Techniques
Content Security Policy is the most effective defense against XSS — when implemented correctly. In practice, CSP configurations are frequently bypassable:
- Overly permissive directives:
script-src 'unsafe-inline'effectively disables CSP's XSS protection entirely.script-src *or wildcards like*.googleapis.comallow loading scripts from attacker-controlled endpoints on those domains. - JSONP endpoints: If the CSP allows scripts from a domain that hosts a JSONP endpoint, the attacker can load arbitrary JavaScript through the JSONP callback parameter. Google APIs, many CDN providers, and analytics services expose JSONP endpoints.
- CDN-hosted libraries with known gadgets: CSP policies that whitelist CDN domains (cdnjs.cloudflare.com, unpkg.com) allow loading any library hosted on those CDNs — including Angular (which provides template injection gadgets), Ember (which has known CSP bypass vectors), and older library versions with known XSS vulnerabilities.
- Base tag injection: If
base-uriis not restricted in the CSP, an attacker can inject a<base>tag that changes the base URL for all relative script sources — causing the application to load scripts from an attacker-controlled server instead of its own.
XSS Types Comparison
| XSS Type | Persistence | Detection Difficulty | Typical Impact | Scanner Detection |
|---|---|---|---|---|
| Reflected | None (URL-based) | Low–Medium | Session hijacking via crafted link | Good |
| Stored | Persistent (database) | Medium | Mass account compromise | Moderate |
| DOM-Based | None (client-side) | High | Session hijacking, data theft | Poor |
| Mutation (mXSS) | Varies | Very High | Sanitizer bypass, full execution | Very Poor |
| Second-Order | Persistent (deferred) | Very High | Admin account takeover | Very Poor |
| File Upload XSS | Persistent (file) | High | Same-origin code execution | Poor |
| Prototype Pollution | Session-based | Very High | Framework-dependent execution | Very Poor |
Why Manual Testing Is Essential
Automated scanners detect reflected XSS in obvious parameters reasonably well. They struggle with everything else. DOM XSS requires analyzing JavaScript execution flow in the browser — tracing data from sources to sinks through event handlers, callbacks, and asynchronous operations. Stored XSS requires submitting payloads in one location and checking rendering in another (often a different user role). Second-order XSS requires understanding that input stored in a user profile might be rendered unsanitized in an admin dashboard two page transitions later.
Manual penetration testing identifies XSS in contexts scanners cannot reach: payloads in file upload metadata that render in admin views, XSS through PDF generation libraries that interpret HTML in user-supplied content, stored XSS in WebSocket messages, DOM XSS in client-side routing logic, and XSS through SVG file uploads served from the application's origin.
The remediation guidance also differs. A scanner reports "XSS found in parameter X — sanitize input." A pentester explains the full exploitation chain, demonstrates the real-world impact (account takeover, not alert(1)), and provides specific remediation — "Use DOMPurify for this rendering context, move uploaded files to a separate cookieless domain, and add a strict CSP with nonce-based script sourcing."
Find the XSS Your Scanner Missed
Lorikeet Security's web application penetration tests include comprehensive XSS testing — reflected, stored, DOM-based, mutation, file upload, and second-order variants. We demonstrate real exploitation impact and provide actionable remediation guidance for every finding.