The bug, in one line
A web application takes input from one user and renders it as part of a page another user sees, without escaping. If that input is <script>alert(1)</script>, the second user's browser dutifully runs it.
The reason XSS is interesting — rather than just a way to pop alert boxes — is that the injected script runs inside the trusted origin of the target site. Same-origin policy doesn't help; the script is from the same origin as the page it landed on. It can read the user's cookies, change what the page says, steal form data as the user types, make requests as the user, and exfiltrate everything to attacker-controlled infrastructure. One injected <script> tag is functionally equivalent to taking over the victim's session on that site.
Three flavors
Reflected
The payload travels in a URL or form field and is echoed back in the response. The attacker emails the victim a crafted link; one click executes the script. Lives only in that single request — but that single request is plenty.
Stored
The payload is saved on the server — in a comment, a profile, a review — and rendered to every visitor who views that page. The attacker injects once and harvests forever. This is the variant that produces self-propagating worms.
DOM-based
The server is innocent. The injection happens entirely in the client's JavaScript: code reads from location.hash or document.referrer and writes it into the DOM with innerHTML. Server-side filters cannot catch what they never see.
Try it — a vulnerable comment board
Post a comment. The page renders it without escaping. Toggle the “safe” mode to see what proper output encoding does to the same payload.
How I Spent My Summer Vacation
What the script can actually do
An injected script runs with the privileges of the origin it landed on. That means:
- Read cookies via
document.cookie— unless the cookie has theHttpOnlyflag, in which case JavaScript cannot read it. Always set HttpOnly on session cookies. - Make authenticated requests with
fetch()orXMLHttpRequest. The browser attaches cookies automatically; the script can change the user's password, transfer money, post messages, anything the API allows. - Rewrite the page — replace the real login form with a phishing one, change account numbers in a banking transfer confirmation, paste a malicious download link into a help article. The user sees what the attacker wants them to see, on a legitimate URL.
- Keylog by listening on input events. Card numbers, passwords, two-factor codes — all available to script that lives on the same page as the form.
- Self-propagate. The classic Samy worm (2005) used a stored XSS on MySpace to add itself to every profile that viewed an infected profile. A million accounts in under twenty hours.
Defense
| Control | What it does | When it helps |
|---|---|---|
| Output encoding | Convert <, >, ", ', & to HTML entities at render time. Context-sensitive: HTML body, HTML attribute, JavaScript string, URL parameter each need different escaping. | The primary defense. Use your framework's auto-escaping (React, Django templates, Razor) and stay inside it. |
| Content Security Policy | An HTTP header that tells the browser which sources it may load scripts from. script-src 'self' kills inline scripts entirely. | Defense in depth: turns successful injection from RCE into a no-op. |
| HttpOnly cookies | Prevents JavaScript from reading the session cookie. | Eliminates the single most damaging XSS outcome (session hijacking). |
| Input validation | Reject input that doesn't match expected shape: usernames, emails, numbers. | Reduces attack surface but is not a substitute for output encoding. |
| Trusted Types (Chrome/Edge) | Refuse to assign strings to dangerous DOM sinks (innerHTML, eval) without a sanitized wrapper. | The most effective defense against DOM-based XSS in modern apps. |
| Framework auto-escaping | React, Vue, Angular, Django all escape by default. The bug almost always involves a developer deliberately bypassing it (dangerouslySetInnerHTML, {!! $x !!}, v-html). | Use the framework. Audit every escape-bypass for justification. |
Notable incidents
- Samy worm — MySpace, 2005. Stored XSS on user profiles. Spread to 1,000,000 accounts in under 20 hours. The author received three years probation.
- TweetDeck XSS worm — 2014. Stored payload in tweet bodies retweeted itself automatically when rendered. Forced TweetDeck offline for hours.
- British Airways / Magecart — 2018. Compromised third-party JavaScript skimmed credit cards from the checkout page for 380,000 customers. Resulted in a £20m ICO fine.
- WordPress plugins, every quarter, forever — XSS is the most-disclosed vulnerability class in the WordPress ecosystem. The bug class is structurally hard to eliminate when plugin authors hand-roll output.