Back to Blog
XSSSecurityFrontendWeb Development

XSS Prevention: Protecting Against Cross-Site Scripting

Prevent XSS attacks in web applications. Learn encoding, sanitization, and Content Security Policy.

B
Bootspring Team
Engineering
February 27, 2026
3 min read

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.

Share this article

Help spread the word about Bootspring