XSS attacks inject malicious scripts into web pages. Here's how to prevent them.
Types of XSS#
1. Reflected XSS
URL: example.com/search?q=<script>alert('xss')</script>
Server reflects input directly in response
2. Stored XSS
Attacker stores payload in database
All users viewing the content execute the script
3. DOM-based XSS
Client-side JavaScript processes untrusted data
Never reaches the server
React Auto-Escaping#
1// ✅ SAFE - React escapes by default
2function UserProfile({ user }) {
3 return <div>{user.name}</div>; // Escaped automatically
4}
5
6// ❌ DANGEROUS - dangerouslySetInnerHTML bypasses escaping
7function Comment({ html }) {
8 return <div dangerouslySetInnerHTML={{ __html: html }} />;
9}
10
11// ✅ SAFER - Sanitize before using dangerouslySetInnerHTML
12import DOMPurify from 'dompurify';
13
14function Comment({ html }) {
15 const clean = DOMPurify.sanitize(html, {
16 ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
17 ALLOWED_ATTR: ['href'],
18 });
19 return <div dangerouslySetInnerHTML={{ __html: clean }} />;
20}URL Handling#
1// ❌ DANGEROUS - javascript: URLs
2function Link({ url, children }) {
3 return <a href={url}>{children}</a>;
4}
5// Attack: <Link url="javascript:alert('xss')" />
6
7// ✅ SAFE - Validate URL protocol
8function SafeLink({ url, children }) {
9 const isValidUrl = (url: string) => {
10 try {
11 const parsed = new URL(url);
12 return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
13 } catch {
14 return false;
15 }
16 };
17
18 if (!isValidUrl(url)) {
19 return <span>{children}</span>;
20 }
21
22 return <a href={url}>{children}</a>;
23}Server-Side Encoding#
1import { encode } from 'html-entities';
2
3// HTML context
4function renderHtml(userInput: string): string {
5 return `<div>${encode(userInput)}</div>`;
6}
7
8// Attribute context
9function renderAttribute(userInput: string): string {
10 const encoded = encode(userInput, { mode: 'attribute' });
11 return `<input value="${encoded}">`;
12}
13
14// JavaScript context - use JSON.stringify
15function renderScript(data: object): string {
16 return `<script>var data = ${JSON.stringify(data)};</script>`;
17}DOM Manipulation Safety#
1// ❌ DANGEROUS - innerHTML with user input
2element.innerHTML = userInput;
3
4// ✅ SAFE - textContent for text
5element.textContent = userInput;
6
7// ✅ SAFE - createElement for structure
8const link = document.createElement('a');
9link.href = sanitizeUrl(userUrl);
10link.textContent = userText;
11element.appendChild(link);
12
13// ❌ DANGEROUS - document.write
14document.write(userInput);
15
16// ❌ DANGEROUS - eval
17eval(userInput);
18new Function(userInput)();
19setTimeout(userInput, 1000);Input Sanitization#
1import DOMPurify from 'dompurify';
2
3// Basic sanitization
4const clean = DOMPurify.sanitize(dirty);
5
6// Custom configuration
7const config = {
8 ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
9 ALLOWED_ATTR: ['href', 'title'],
10 ALLOW_DATA_ATTR: false,
11 ADD_TAGS: ['custom-element'],
12 FORBID_TAGS: ['script', 'style'],
13 FORBID_ATTR: ['onerror', 'onclick'],
14};
15
16const clean = DOMPurify.sanitize(dirty, config);
17
18// Hooks for custom processing
19DOMPurify.addHook('afterSanitizeAttributes', (node) => {
20 if (node.tagName === 'A') {
21 node.setAttribute('rel', 'noopener noreferrer');
22 node.setAttribute('target', '_blank');
23 }
24});Content Security Policy#
1// Prevent inline script execution
2res.setHeader(
3 'Content-Security-Policy',
4 "script-src 'self'; object-src 'none'"
5);
6
7// With nonces for inline scripts
8const nonce = crypto.randomBytes(16).toString('base64');
9res.setHeader(
10 'Content-Security-Policy',
11 `script-src 'self' 'nonce-${nonce}'`
12);HTTP-Only Cookies#
1// Prevent JavaScript access to sensitive cookies
2res.cookie('session', token, {
3 httpOnly: true, // Not accessible via document.cookie
4 secure: true, // HTTPS only
5 sameSite: 'strict',
6});Template Engines#
1// EJS - escape by default
2<%= userInput %> // Escaped
3<%- userInput %> // Raw (dangerous)
4
5// Handlebars - escape by default
6{{userInput}} // Escaped
7{{{userInput}}} // Raw (dangerous)
8
9// Pug/Jade - escape by default
10p= userInput // Escaped
11p!= userInput // Raw (dangerous)Testing for XSS#
1const xssPayloads = [
2 '<script>alert("xss")</script>',
3 '<img src=x onerror=alert("xss")>',
4 '<svg onload=alert("xss")>',
5 'javascript:alert("xss")',
6 '<a href="javascript:alert(\'xss\')">click</a>',
7 '"><script>alert("xss")</script>',
8 "'-alert('xss')-'",
9];
10
11describe('XSS Prevention', () => {
12 for (const payload of xssPayloads) {
13 it(`should sanitize: ${payload.slice(0, 30)}...`, () => {
14 const result = sanitize(payload);
15 expect(result).not.toContain('<script');
16 expect(result).not.toContain('onerror');
17 expect(result).not.toContain('javascript:');
18 });
19 }
20});Combine output encoding, input sanitization, and CSP for defense in depth.