Cross-Site Scripting (XSS) allows attackers to inject malicious JavaScript into web pages viewed by other users. It's one of the most common web vulnerabilities, accounting for nearly 40% of all cyberattacks.
Types of XSS Attacks
1. Reflected XSS
Malicious script is reflected off a web server, typically via URL parameters.
// Vulnerable search page
app.get('/search', (req, res) => {
const query = req.query.q;
res.send(`<h1>Results for: ${query}</h1>`);
});
// Attack URL:
// /search?q=<script>alert(document.cookie)</script>
2. Stored XSS
Malicious script is permanently stored on the server (database, comment system, etc.).
// Vulnerable comment system
app.post('/comment', async (req, res) => {
const comment = req.body.comment;
await db.insert({ text: comment }); // Stored!
res.redirect('/comments');
});
// Later, when displaying comments:
comments.forEach(c => {
html += `<div>${c.text}</div>`; // XSS executed!
});
3. DOM-Based XSS
Vulnerability exists in client-side code, not server-side.
// Vulnerable client-side code
const name = new URLSearchParams(location.search).get('name');
document.getElementById('welcome').innerHTML = `Hello ${name}!`;
// Attack URL:
// /?name=<img src=x onerror=alert(document.cookie)>
Common XSS Attack Vectors
Script Tags
<script>alert('XSS')</script>
Event Handlers
<img src=x onerror=alert('XSS')>
<body onload=alert('XSS')>
<svg/onload=alert('XSS')>
JavaScript URIs
<a href="javascript:alert('XSS')">Click</a>
<iframe src="javascript:alert('XSS')"></iframe>
Data URIs
<object data="data:text/html,<script>alert('XSS')</script>"></object>
Prevention Techniques
1. Output Encoding (Primary Defense)
Always encode user input when displaying it.
HTML Context:
// ❌ Vulnerable
res.send(`<div>${userInput}</div>`);
// ✅ Safe: HTML entity encoding
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
res.send(`<div>${escapeHtml(userInput)}</div>`);
JavaScript Context:
// ❌ Vulnerable
<script>const name = "${userInput}";</script>
// ✅ Safe: JSON.stringify
<script>const name = ${JSON.stringify(userInput)};</script>
URL Context:
// ❌ Vulnerable
<a href="/search?q=${query}">Search</a>
// ✅ Safe: URL encoding
<a href="/search?q=${encodeURIComponent(query)}">Search</a>
2. Use Template Engines with Auto-Escaping
React (auto-escapes by default):
// ✅ Safe: React escapes automatically
function Welcome({ name }) {
return <h1>Hello {name}!</h1>;
}
// ⚠️ Dangerous: Only use for trusted HTML
function DangerousHTML({ html }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
Vue.js:
<!-- ✅ Safe: Auto-escaped -->
<div>{{ userInput }}</div>
<!-- ⚠️ Dangerous: Raw HTML -->
<div v-html="userInput"></div>
EJS:
<!-- ✅ Safe: Auto-escaped -->
<div><%= userInput %></div>
<!-- ⚠️ Dangerous: Raw HTML -->
<div><%- userInput %></div>
3. Content Security Policy (CSP)
Whitelist allowed script sources to prevent inline script execution.
// Express middleware
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' https://trusted-cdn.com; object-src 'none';"
);
next();
});
Strict CSP (best practice):
Content-Security-Policy:
script-src 'nonce-{random}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
4. Sanitize User HTML (When Needed)
If you must allow user HTML (rich text editors), use a sanitizer library.
DOMPurify (client-side):
import DOMPurify from 'dompurify';
const dirty = '<img src=x onerror=alert("XSS")>';
const clean = DOMPurify.sanitize(dirty);
// Result: <img src="x">
sanitize-html (Node.js):
const sanitizeHtml = require('sanitize-html');
const dirty = '<script>alert("XSS")</script><p>Safe content</p>';
const clean = sanitizeHtml(dirty, {
allowedTags: ['p', 'b', 'i', 'em', 'strong', 'a'],
allowedAttributes: {
'a': ['href']
}
});
// Result: <p>Safe content</p>
5. Input Validation
While not a primary XSS defense, validation adds defense-in-depth.
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new Error('Invalid email');
}
// Allowlist validation for specific inputs
const allowedColors = ['red', 'blue', 'green'];
if (!allowedColors.includes(color)) {
throw new Error('Invalid color');
}
6. HttpOnly Cookies
Prevent JavaScript access to session cookies.
res.cookie('sessionId', value, {
httpOnly: true, // Not accessible via JavaScript
secure: true, // HTTPS only
sameSite: 'strict' // CSRF protection
});
Framework-Specific Protection
React
// ✅ Safe by default
<div>{userInput}</div>
// ❌ Only when you REALLY need raw HTML
<div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />
Angular
<!-- ✅ Safe: Auto-sanitized -->
<div>{{ userInput }}</div>
<!-- ✅ Safe: DomSanitizer for trusted HTML -->
<div [innerHTML]="sanitizer.bypassSecurityTrustHtml(trustedHtml)"></div>
Express.js
// Use templating engines with auto-escape
app.set('view engine', 'ejs');
// Or manually escape
const escape = require('escape-html');
res.send(`<div>${escape(userInput)}</div>`);
Testing for XSS
Manual Testing
Test with common XSS payloads:
<script>alert('XSS')</script>
<img src=x onerror=alert('XSS')>
<svg/onload=alert('XSS')>
javascript:alert('XSS')
<iframe src="javascript:alert('XSS')"></iframe>
Automated Scanning
- OWASP ZAP: Free security scanner
- Burp Suite: Professional web vulnerability scanner
- ScanMyCode.dev: AI-powered code audit checks for XSS vulnerabilities
Real-World XSS Examples
Twitter 2010
Reflected XSS in search allowed attackers to create self-retweeting worms.
TweetDeck 2014
Stored XSS in tweets caused widespread auto-retweets and account compromises.
British Airways 2018
XSS attack on payment page led to theft of 380,000 payment cards. Fine: £183M.
XSS Prevention Checklist
- ✅ Encode ALL user input on output
- ✅ Use templating engines with auto-escaping
- ✅ Implement Content Security Policy
- ✅ Never use
eval()orinnerHTMLwith user input - ✅ Sanitize HTML if rich text is needed
- ✅ Use HttpOnly cookies for sessions
- ✅ Validate input (defense-in-depth)
- ✅ Regular security testing and audits
- ✅ Keep frameworks and dependencies updated
Conclusion
XSS is preventable with proper output encoding and security headers. Don't trust user input, ever. Treat all data as untrusted until properly encoded for the context in which it's used.
Want to ensure your code is free from XSS vulnerabilities? Get a security audit and receive a comprehensive XSS analysis within 24 hours.