XSS Prevention Overview
Implement comprehensive Cross-Site Scripting (XSS) prevention using input sanitization, output encoding, CSP headers, and secure coding practices.
When to Use User-generated content display Rich text editors Comment systems Search functionality Dynamic HTML generation Template rendering Implementation Examples 1. Node.js XSS Prevention // xss-prevention.js const createDOMPurify = require('dompurify'); const { JSDOM } = require('jsdom'); const he = require('he');
const window = new JSDOM('').window; const DOMPurify = createDOMPurify(window);
class XSSPrevention { /* * HTML Entity Encoding - Safest for text content / static encodeHTML(str) { return he.encode(str, { useNamedReferences: true, encodeEverything: false }); }
/* * Sanitize HTML - For rich content / static sanitizeHTML(dirty) { const config = { ALLOWED_TAGS: [ 'p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'a', 'img', 'blockquote', 'code' ], ALLOWED_ATTR: [ 'href', 'src', 'alt', 'title', 'class' ], ALLOWED_URI_REGEXP: /^(?:https?|mailto):/i, KEEP_CONTENT: true, RETURN_DOM: false, RETURN_DOM_FRAGMENT: false };
return DOMPurify.sanitize(dirty, config);
}
/* * Strict sanitization - For untrusted HTML / static sanitizeStrict(dirty) { return DOMPurify.sanitize(dirty, { ALLOWED_TAGS: ['b', 'i', 'em', 'strong'], ALLOWED_ATTR: [], KEEP_CONTENT: true }); }
/* * JavaScript context encoding / static encodeForJS(str) { return str.replace(/[<>"'&]/g, (char) => { const escape = { '<': '\x3C', '>': '\x3E', '"': '\x22', "'": '\x27', '&': '\x26' }; return escape[char]; }); }
/* * URL parameter encoding / static encodeURL(str) { return encodeURIComponent(str); }
/* * Attribute context encoding / static encodeAttribute(str) { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') .replace(/\//g, '/'); }
/* * Validate and sanitize URLs / static sanitizeURL(url) { try { const parsed = new URL(url);
// Only allow safe protocols
if (!['http:', 'https:', 'mailto:'].includes(parsed.protocol)) {
return '';
}
return parsed.href;
} catch {
return '';
}
}
/ * Strip all HTML tags / static stripHTML(str) { return str.replace(/<[^>]>/g, ''); }
/* * React-style JSX escaping / static escapeForReact(str) { return { __html: DOMPurify.sanitize(str) }; } }
// Express middleware function xssProtection(req, res, next) { // Sanitize request body if (req.body) { req.body = sanitizeObject(req.body); }
// Sanitize query parameters if (req.query) { req.query = sanitizeObject(req.query); }
next(); }
function sanitizeObject(obj) { const sanitized = {};
for (const [key, value] of Object.entries(obj)) { if (typeof value === 'string') { sanitized[key] = XSSPrevention.stripHTML(value); } else if (typeof value === 'object' && value !== null) { sanitized[key] = sanitizeObject(value); } else { sanitized[key] = value; } }
return sanitized; }
// Express example const express = require('express'); const app = express();
app.use(express.json()); app.use(xssProtection);
app.post('/api/comments', (req, res) => { const { comment } = req.body;
// Additional sanitization for rich content const safeComment = XSSPrevention.sanitizeHTML(comment);
// Store in database // db.comments.insert({ content: safeComment });
res.json({ comment: safeComment }); });
module.exports = XSSPrevention;
- Python XSS Prevention
xss_prevention.py
import html import bleach from urllib.parse import urlparse, quote import re
class XSSPrevention: # Allowed HTML tags for rich content ALLOWED_TAGS = [ 'p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'a', 'blockquote', 'code' ]
ALLOWED_ATTRIBUTES = {
'a': ['href', 'title'],
'img': ['src', 'alt']
}
@staticmethod
def encode_html(text: str) -> str:
"""HTML entity encoding - safest for text content"""
return html.escape(text, quote=True)
@staticmethod
def sanitize_html(dirty_html: str) -> str:
"""Sanitize HTML - for rich content"""
return bleach.clean(
dirty_html,
tags=XSSPrevention.ALLOWED_TAGS,
attributes=XSSPrevention.ALLOWED_ATTRIBUTES,
strip=True
)
@staticmethod
def sanitize_strict(dirty_html: str) -> str:
"""Strict sanitization - strip all HTML"""
return bleach.clean(
dirty_html,
tags=[],
attributes={},
strip=True
)
@staticmethod
def strip_html(text: str) -> str:
"""Remove all HTML tags"""
return re.sub(r'<[^>]*>', '', text)
@staticmethod
def sanitize_url(url: str) -> str:
"""Validate and sanitize URLs"""
try:
parsed = urlparse(url)
# Only allow safe protocols
if parsed.scheme not in ['http', 'https', 'mailto']:
return ''
return url
except:
return ''
@staticmethod
def encode_for_javascript(text: str) -> str:
"""Encode for JavaScript context"""
escape_map = {
'<': '\\x3C',
'>': '\\x3E',
'"': '\\x22',
"'": '\\x27',
'&': '\\x26',
'/': '\\x2F'
}
return ''.join(escape_map.get(char, char) for char in text)
@staticmethod
def encode_url_param(text: str) -> str:
"""Encode for URL parameters"""
return quote(text, safe='')
Flask integration
from flask import Flask, request, jsonify from functools import wraps
app = Flask(name)
def sanitize_input(f): """Decorator to sanitize all request inputs""" @wraps(f) def decorated_function(args, *kwargs): if request.is_json: data = request.get_json() request._cached_json = sanitize_dict(data)
return f(*args, **kwargs)
return decorated_function
def sanitize_dict(data: dict) -> dict: """Recursively sanitize dictionary values""" sanitized = {}
for key, value in data.items():
if isinstance(value, str):
sanitized[key] = XSSPrevention.strip_html(value)
elif isinstance(value, dict):
sanitized[key] = sanitize_dict(value)
elif isinstance(value, list):
sanitized[key] = [
sanitize_dict(item) if isinstance(item, dict)
else XSSPrevention.strip_html(item) if isinstance(item, str)
else item
for item in value
]
else:
sanitized[key] = value
return sanitized
@app.route('/api/comments', methods=['POST']) @sanitize_input def create_comment(): data = request.get_json() comment = data.get('comment', '')
# Additional rich content sanitization
safe_comment = XSSPrevention.sanitize_html(comment)
return jsonify({'comment': safe_comment})
Django template filter
from django import template from django.utils.safestring import mark_safe
register = template.Library()
@register.filter(name='sanitize_html') def sanitize_html_filter(value): """Django template filter for HTML sanitization""" sanitized = XSSPrevention.sanitize_html(value) return mark_safe(sanitized)
Usage in templates:
{{ user_content|sanitize_html }}
- React XSS Prevention // XSSSafeComponent.jsx import React from 'react'; import DOMPurify from 'dompurify';
// Safe text rendering (React automatically escapes) function SafeText({ text }) { return
// Sanitized HTML rendering function SafeHTML({ html }) { const sanitized = DOMPurify.sanitize(html, { ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'a'], ALLOWED_ATTR: ['href'] });
return (
); }// Safe URL attribute function SafeLink({ href, children }) { const safeHref = sanitizeURL(href);
return ( {children} ); }
function sanitizeURL(url) { try { const parsed = new URL(url);
if (!['http:', 'https:'].includes(parsed.protocol)) {
return '';
}
return parsed.href;
} catch { return ''; } }
// Input sanitization hook function useSanitizedInput(initialValue = '') { const [value, setValue] = React.useState(initialValue);
const handleChange = (e) => { const sanitized = DOMPurify.sanitize(e.target.value, { ALLOWED_TAGS: [], KEEP_CONTENT: true });
setValue(sanitized);
};
return [value, handleChange]; }
// Usage function CommentForm() { const [comment, handleCommentChange] = useSanitizedInput();
const handleSubmit = async (e) => { e.preventDefault();
await fetch('/api/comments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ comment })
});
};
return (
); }export { SafeText, SafeHTML, SafeLink, useSanitizedInput };
- Content Security Policy // csp-config.js const helmet = require('helmet');
function setupCSP(app) { app.use(helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"],
// Only allow scripts from trusted sources
scriptSrc: [
"'self'",
"'nonce-RANDOM_NONCE'", // Use dynamic nonces
"https://cdn.example.com"
],
// Styles
styleSrc: [
"'self'",
"'nonce-RANDOM_NONCE'",
"https://fonts.googleapis.com"
],
// No inline styles/scripts
objectSrc: ["'none'"],
baseUri: ["'self'"],
// Report violations
reportUri: ['/api/csp-violations']
}
}));
// CSP violation reporter app.post('/api/csp-violations', (req, res) => { console.error('CSP Violation:', req.body); res.status(204).end(); }); }
// Generate nonce for inline scripts function generateNonce() { return require('crypto').randomBytes(16).toString('base64'); }
// Express middleware to add nonce app.use((req, res, next) => { res.locals.nonce = generateNonce(); next(); });
// In templates: <script nonce="<%= nonce %>">
Best Practices ✅ DO Encode output by default Use templating engines Implement CSP headers Sanitize rich content Validate URLs Use HTTPOnly cookies Regular security testing Use secure frameworks ❌ DON'T Trust user input Use innerHTML directly Skip output encoding Allow inline scripts Use eval() Mix contexts (HTML/JS) XSS Types Reflected: Immediate response Stored: Persisted in database DOM-based: Client-side manipulation Mutation-based: Parser differences Context-Specific Encoding HTML Content: HTML entity encoding HTML Attribute: Attribute encoding JavaScript: JavaScript escaping URL: URL encoding CSS: CSS escaping Resources OWASP XSS Prevention DOMPurify Content Security Policy