What Is Cross-Site Scripting (XSS)?
Cross-site scripting (XSS) is a web vulnerability that lets an attacker run their own JavaScript in another user's browser, inside the security context of a trusted site. It happens when a web application takes data controlled by an attacker and includes it in a page without properly separating that data from executable code.
What XSS actually is
The browser's same-origin policy assumes that any script delivered by a page came from that page's developers and can be trusted with the page's cookies, DOM, and session. XSS breaks that assumption. If an attacker can get the browser to treat their input as script, that script inherits all the privileges of the legitimate origin.
The root cause is almost always a confusion between data and code. A username, comment, search term, or URL parameter is data. When that data is dropped into HTML, an attribute, or a <script> block without escaping, the browser's parser can be tricked into interpreting parts of it as markup or executable code instead of plain text.
How XSS works
Consider a search page that echoes the query back to the user. A naive implementation might build the response like this:
<p>No results for: <?= $_GET['q'] ?></p>
If a visitor requests ?q=hello, the page reads "No results for: hello". But if an attacker crafts a link with ?q=<script>alert(document.cookie)</script>, the raw markup is written straight into the document. The browser parses the <script> tag and executes it. Because the script runs on the real site's origin, it can read cookies, local storage, and the full DOM.
From there an attacker can steal session tokens and send them to a server they control, rewrite the page to phish for passwords, perform actions as the logged-in user, or chain into a fuller account takeover. Tokens stored in JavaScript-accessible places are especially exposed; you can inspect a captured token with a JWT Decoder to see exactly what an attacker would gain.
The three main types of XSS
Reflected XSS
The malicious input is part of the request and is immediately reflected in the response, as in the search example above. It is not stored anywhere, so the attacker must deliver the crafted URL to a victim, usually through a link in email, chat, or another site. The payload only fires for users who follow that specific link.
Stored XSS
The payload is saved on the server, in a database, comment thread, profile field, or log, and served to every user who views the affected page. Stored XSS is generally the most dangerous variant because it needs no per-victim delivery and can hit many users automatically, including administrators viewing a dashboard.
DOM-based XSS
The vulnerability lives entirely in client-side JavaScript. The server may return a perfectly safe page, but a script reads attacker-controlled data from a source like location.hash or document.referrer and writes it into a dangerous sink such as innerHTML, document.write, or eval. Because the flaw never round-trips through the server, server-side filters cannot see or stop it.
Why XSS matters
XSS has been a fixture of the OWASP Top 10 for years and remains one of the most common findings in web security assessments. Its impact is broad because it executes with the victim's identity: anything the user can do, the injected script can do. That includes transferring funds, changing account settings, exfiltrating private data, or spreading itself.
Self-propagating stored XSS has produced real worms. The 2005 Samy worm on MySpace added over a million friend connections within roughly a day by injecting JavaScript into user profiles that copied itself to anyone who viewed an infected page. The episode is a clear demonstration that a single missed escape on a high-traffic page can cascade across an entire platform.
Defending against XSS
Context-aware output encoding
The primary defense is encoding untrusted data for the exact context where it appears. HTML body, HTML attributes, JavaScript strings, CSS, and URLs each require different escaping. In an HTML body, characters like <, >, &, and quotes should become entities such as < and > so the parser treats them as text. You can see how raw characters map to their safe equivalents with an HTML Entity Encoder/Decoder, and experiment with how different payloads are neutralized using an XSS Payload Encoder.
Safe APIs over string building
Prefer APIs that treat input as data by design. Setting element.textContent never parses HTML, whereas element.innerHTML does. Modern frameworks like React, Angular, and Vue auto-escape interpolated values, which is why XSS in these apps usually appears only where developers opt out (for example, React's dangerouslySetInnerHTML). When you must render user-supplied HTML, run it through a vetted sanitizer rather than a hand-rolled blocklist.
Content Security Policy
A Content Security Policy is a defense-in-depth HTTP header that restricts which scripts the browser will execute. A strict policy can block inline scripts and only allow scripts from approved sources or those carrying a valid nonce, so even if an injection slips through, the payload may never run. CSP does not replace encoding, but it sharply limits the blast radius. Build a starter policy with a CSP Builder and see the full approach in our Content Security Policy guide.
Hardening cookies
Marking session cookies HttpOnly prevents JavaScript from reading them via document.cookie, which blocks the classic cookie-theft payload. Combine it with the Secure and SameSite attributes for stronger session protection.
Common pitfalls
Several patterns repeatedly fail. Blocklist filters that strip the literal string <script> are trivially bypassed, because XSS also fires through event-handler attributes like onerror, javascript: URLs, and many other vectors. Encoding for the wrong context is another trap: HTML-escaping a value that lands inside a JavaScript string or an unquoted attribute leaves it exploitable. Treating data as "already safe" because it came from your own database ignores stored XSS entirely; encode at the point of output, every time, regardless of source.
XSS is sometimes confused with other injection and cross-origin issues. It is distinct from server-side injection like SQL injection, though both stem from mixing untrusted input with a parsed language, and it is unrelated to CORS, which governs cross-origin requests rather than script execution. Understanding the injection mindset in general helps; the same data-versus-code confusion drives the SQL Injection Demo. The reliable rule across all of them is simple: never trust input, and always encode output for its destination.
Frequently Asked Questions
Cross-site scripting (XSS) is a vulnerability that lets an attacker run their own JavaScript in someone else's browser on a trusted website. The script runs with the site's privileges, so it can read cookies, change the page, or act as the logged-in user.
Reflected XSS echoes attacker input from the request straight back into the response. Stored XSS saves the payload on the server so it hits every viewer of a page. DOM-based XSS happens entirely in client-side JavaScript when a script writes untrusted data into a dangerous sink like innerHTML.
The main defense is context-aware output encoding: escape untrusted data for the exact place it appears, whether HTML body, attribute, or JavaScript. Add a Content Security Policy, use safe APIs like textContent, sanitize any user-supplied HTML, and mark session cookies HttpOnly.
No. Both come from mixing untrusted input with code, but XSS runs in the victim's browser and targets the front end, while SQL injection runs against the database on the server. They require different fixes, though the underlying lesson is the same: never trust input.
Stored XSS is saved on the server and served automatically to everyone who views the affected page, so it needs no per-victim link delivery and can affect many users, including administrators. Reflected XSS only fires for someone who follows a specifically crafted URL.