Cross-Site Scripting Exploitation in Modern Single-Page Applications
Modern frameworks don't stop XSS—SPAs just moved the attack surface.

Cross-site scripting didn't go away when React, Vue, and Angular took over the frontend world. It moved. Plenty of engineers believe the opposite: that auto-escaping built into modern frameworks closed the book on XSS, and that the bug belongs to an older era of legacy templates and raw string concatenation. That belief is backward, and the architecture of a single-page application is the reason why.
Why XSS didn't die when SPAs took over
But the assumption that this solved the problem gets the shape of the problem wrong. The bug didn't shrink. It spread into new places that didn't exist when most output happened server-side in a single templating language and escaping that one layer was most of the job.
A modern SPA looks nothing like that old model. It might render part of a page on the server, hydrate the rest on the client, pull in JSON from three different APIs, and hand pieces of that data to a client-side template engine, and every one of those steps is its own output context with its own escaping rules. Each context is a place where a developer has to get the encoding right, and each one is a place where a single missed step reopens the door. Client-side templates, SPA routers, JSON that gets echoed straight into a script tag, markdown renderers, and the stray innerHTML call that slipped past code review all count as injection points that simply didn't exist in a traditional server-rendered app.
Frameworks still auto-escape on their happy path. But developers keep reaching past that happy path for escape hatches: dangerouslySetInnerHTML in React, raw innerHTML in plain JavaScript, eval(), document.write(). Every one of those bypasses the protection the framework was built to provide. HackerDNA's 2026 attack guide puts a number behind the result: XSS remains one of the most reported findings in bug bounty programs, despite years of framework-level improvement. The Security Journey 2026 analysis points to why: a wider attack surface created by complex frontend frameworks, more AI-generated code shipping without careful review, and security practices that vary widely from one developer to the next.
The XSS class SPAs made worse
Every form of XSS comes down to the same root cause: data an attacker controls reaches an interpreter, HTML, JavaScript, or otherwise, without being encoded for the context it lands in. What separates the three classes is where that payload sits while it waits to fire, and that location decides whether a server-side defense ever gets a chance to see it.
Reflected XSS carries its payload inside the current request: a URL parameter, a form field, an HTTP header. The server takes that value and echoes it straight back into the response, and because the attack depends on a single request-response cycle, it needs a delivery step, usually a crafted link the victim has to click.
Stored XSS works differently. The payload gets saved on the server, inside a comment, a profile bio, a support ticket, a product review, and from that point on it goes out to every visitor who loads that page. No link to click, no social engineering required. The victim just opens a normal page, and the script is already sitting there waiting. The 2025 ERPNext/Frappe stored XSS, tracked as CVE-2025-56379, shows how this plays out in production software: the Blog module let authenticated users inject malicious HTML and JavaScript into content fields, and that code ran in the browser of every single reader who opened the post.
DOM-based XSS is the class that SPA architecture turned from a rare edge case into routine business. Here, client-side JavaScript takes data from somewhere untrusted, window.location, document.referrer, location.hash, a URL query parameter, and writes it into the page without sanitizing it first. The server never even sees the payload. Because a DOM XSS payload can live in the URL fragment after the # symbol, it never gets sent to the server at all, which leaves server-side filters and a large share of web application firewalls structurally blind to it. There's nothing for them to inspect. SPAs make this common because they lean on client-side routing, update the DOM dynamically, and pull data through API calls, and JavaScript running in the browser handles all of it directly, not a server template that a security team can audit in one place.
Sources, sinks, and the paths between them in a SPA
Every DOM XSS bug needs exactly two ingredients to exist: a source, which is the point where untrusted data enters JavaScript, and a sink, which is a dangerous function that writes that data into the DOM or runs it as code. Whether the bug is actually exploitable comes down to one question: is there an unbroken, unsanitized path connecting that source to that sink?
Sources commonly appear in SPA code as URL parameters, URL fragments, and data from third-party APIs. URL parameters read via URLSearchParams are one of the most common. Or it can be data that comes back from a third-party API and gets dropped into the page without encoding. Neither looks dangerous on its own. The danger occurs once that value reaches a sink.
Sinks are the functions that turn data into behavior. innerHTML, outerHTML, and insertAdjacentHTML() all write raw HTML straight into the DOM, no questions asked, and Asif Ebrahim's guide to DOM XSS in SPAs lists these among the dangerous APIs to watch closely.
A small example makes the path obvious. Picture a SPA that reads a URL parameter with URLSearchParams and assigns that value directly to the innerHTML of a greeting element on the page. Visit that page with a crafted ?name= parameter, and the payload runs the instant the page loads, with no server involved at any point. A jQuery version of the same bug looks just as simple: a payload read from location.search gets passed directly into $('#output').html(payload). Because that payload never appears inside an HTTP request body, it slides past many traditional web application firewalls without triggering a single rule.
Client-side routing widens this gap even further. SPAs parse URL fragments and act on them directly in JavaScript, which creates a whole category of sources that a scanner watching HTTP traffic was never built to see in the first place.
Why "alert(1)" understates the real impact of XSS in SPAs
A popup box that says "1" is how XSS gets demonstrated in a classroom.
The clearest picture of the real stakes starts with stored XSS sitting inside an admin interface, which is the highest-severity version of this bug. An attacker drops a payload into a comment field or a support ticket, and the code sits there until an administrator opens the moderation queue to review it. The moment that page loads in the admin's browser, the script runs with the admin's own privileges, handing the attacker admin-level access with no password guessed, no phishing email sent, and no malware installed.
The primary impact in SPA environments is session token theft and authenticated request forgery. Even in cases where the attacker never touches a cookie directly, a script running inside the victim's session can fire off authenticated requests on the victim's behalf: changing an email address, adding a new admin account, pulling data out of the application, because that script has full run of the DOM and the victim's credentials are already attached to every request it sends. HttpOnly cookies cut down on direct cookie theft, but they don't stop a script from acting as the logged-in user. That protection is a mitigation rather than a fix.
From there, attackers chain XSS with other weaknesses to turn a browser-level bug into something that compromises the whole application. If you pair XSS with weak CSRF protections, an attacker can make state changes across origins that should never have been possible. Pairing it with a server-side bug turns what started as a script running in one browser tab into full application compromise.
The 2026 payload cheat sheet lays out just how far the goals of a modern XSS attack have moved past a simple alert box. Cryptojacking scripts mine cryptocurrency quietly in the background of a victim's browser session. Fake login overlays sit cloned on top of a real page to harvest credentials. Hijacking window.location turns the bug into a phishing delivery mechanism. Exploitation frameworks hook the browser for persistent, ongoing control. Supply chain attacks through compromised third-party libraries can hit every single visitor to a site at once, because the SPA already trusts that library's code by default. In June 2024, the Polyfill.io JavaScript injection attack showed how far that kind of compromise can spread, and it hit a vast number of websites that had simply trusted one shared script. The 2026 cheat sheet also points to a shift in how these attacks stay hidden: with SPAs now the dominant architecture on the web, attackers craft payloads that mimic normal traffic patterns or route code fetches and stolen data through third-party services the application already trusts, which makes the whole thing far harder to spot.
Why automated scanners routinely miss DOM XSS in SPAs
Reflected XSS is the one class automated scanners handle reasonably well. But DOM XSS, and stored XSS that needs a second user or a specific workflow to trigger, routinely slip past the same tools.
The reason comes down to where these scanners look. Most of them watch HTTP traffic: the requests and responses moving between the browser and the server. DOM XSS payloads never appear in that traffic, because the entire vulnerable path, from source to sink, stays inside client-side JavaScript execution. A payload sitting in the URL hash fragment after # never gets transmitted in an HTTP request in the first place, so a scanner watching network traffic has nothing unusual to flag. Client-side routing compounds the problem: the "page" changes in a SPA without any server round-trip happening at all, so a scanner built to probe URLs simply never discovers routes that exist only inside JavaScript.
Even newer defenses struggle here. AI-driven web application firewalls and next-generation content security policies use behavioral analysis, so they can catch payloads that have been obfuscated to dodge simple pattern matching. Attackers have adapted anyway, crafting payloads that look like ordinary traffic. When testing combined HTTP Parameter Pollution with JavaScript injection, it found high bypass rates against the WAF configurations tested, and entire major cloud WAF rule sets were bypassed outright in some cases. Detection built around known patterns keeps losing ground, because attackers have learned what those patterns look like.
Stored XSS adds its own layer of difficulty when it depends on an admin reviewing content or a multi-step workflow completing before the payload fires. To catch that kind of bug, a tester has to understand how data moves through the application, and automated tools simply don't model that. None of this is a knock on automated tooling as a category. It's a structural limit: finding DOM XSS in a SPA takes a tester who understands how JavaScript executes on the client side, reads the actual source code, and manually traces data from where it enters to where it fires, work that a tool watching network traffic cannot do.
The tester's methodology for finding DOM XSS in a SPA
Finding DOM XSS in a SPA comes down to working through the application's JavaScript and mapping every source to every sink it can reach, which makes it as much a code-reading exercise as it is a hands-on probing one.
The first step is enumerating sources: finding every place in the application where untrusted data gets read in. That means searching the JavaScript codebase for location.href, location.hash, location.search, document.referrer, document.URL, and any use of URLSearchParams. It also means identifying every API endpoint whose response ends up rendered into the DOM, and checking how each value coming back from that response gets handled once it arrives.
From there, the work shifts to enumerating sinks: every place in the code where data gets written into the DOM or executed outright. Framework-specific escape hatches deserve particular attention here, since they're where a framework's built-in protection gets deliberately switched off: dangerouslySetInnerHTML in React, v-html in Vue, and bypassSecurityTrustHtml in Angular all tell a tester exactly where a developer decided to bypass the framework's default safety net.
Doing this well depends heavily on what kind of access a tester has going in. Without that access, testing has to rely on browser DevTools, setting JavaScript breakpoints, and a good deal of trial and error to find the same paths from the outside.
The methodology closes with proof. Once a plausible source-to-sink path turns up, the tester crafts a payload and confirms it fires, and that turns a theoretical data-flow path into a working proof of concept that demonstrates the bug beyond doubt.


