| name | xss-prevention |
| description | Prevent Cross-Site Scripting (XSS) attacks through input sanitization, output encoding, and Content Security Policy. Use when handling user-generated content in web applications. |
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, ''')
.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;
2. 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 }}
3. React XSS Prevention
// XSSSafeComponent.jsx
import React from 'react';
import DOMPurify from 'dompurify';
// Safe text rendering (React automatically escapes)
function SafeText({ text }) {
return <div>{text}</div>;
}
// Sanitized HTML rendering
function SafeHTML({ html }) {
const sanitized = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'a'],
ALLOWED_ATTR: ['href']
});
return (
<div
dangerouslySetInnerHTML={{ __html: sanitized }}
/>
);
}
// Safe URL attribute
function SafeLink({ href, children }) {
const safeHref = sanitizeURL(href);
return (
<a
href={safeHref}
rel="noopener noreferrer"
target="_blank"
>
{children}
</a>
);
}
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 (
<form onSubmit={handleSubmit}>
<textarea
value={comment}
onChange={handleCommentChange}
placeholder="Enter comment"
/>
<button type="submit">Submit</button>
</form>
);
}
export { SafeText, SafeHTML, SafeLink, useSanitizedInput };
4. 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