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