
Xss Prevention
- 184 installs
- 202 repo stars
- Updated August 4, 2026
- secondsky/claude-skills
Helps with ai & agent building tasks during AI-assisted development.
About
xss-prevention is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- xss-prevention
- AI & Agent Building
- AI-coding skill
Xss Prevention by the numbers
- 184 all-time installs (skills.sh)
- +19 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #3,015 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/secondsky/claude-skills --skill xss-preventionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 184 |
|---|---|
| repo stars | ★ 202 |
| Last updated | August 4, 2026 |
| Repository | secondsky/claude-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
XSS Prevention
Overview
Implement comprehensive Cross-Site Scripting attack prevention through input sanitization, output encoding, Content Security Policy headers, and secure coding practices.
When to Use
- User-generated content display
- Rich text editors
- Comment systems
- Search functionality
- Dynamic HTML generation
- Template rendering scenarios
XSS Attack Types
| Type | Vector | Defense |
|---|---|---|
| Reflected | URL parameters | Output encoding |
| Stored | Database content | Input sanitization |
| DOM-based | Client-side JS | Safe DOM APIs |
| Mutation | HTML parser quirks | Strict sanitization |
Output Encoding (Node.js)
function encodeHTML(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function encodeForAttribute(str) {
return str.replace(/[^\w.-]/g, char =>
`&#x${char.charCodeAt(0).toString(16)};`
);
}
// Usage in templates
app.get('/profile', (req, res) => {
const username = encodeHTML(req.query.name);
res.send(`<h1>Welcome, ${username}</h1>`);
});DOMPurify Sanitization
import DOMPurify from 'dompurify';
const config = {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'title'],
ALLOW_DATA_ATTR: false
};
function sanitizeHTML(dirty) {
return DOMPurify.sanitize(dirty, config);
}
// React component
function RichContent({ html }) {
return (
<div dangerouslySetInnerHTML={{ __html: sanitizeHTML(html) }} />
);
}Content Security Policy
// Express middleware
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}'`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'"
].join('; '));
next();
});Safe DOM APIs
// DANGEROUS - avoid these
element.innerHTML = userInput; // XSS risk
element.outerHTML = userInput; // XSS risk
document.write(userInput); // XSS risk
eval(userInput); // Code injection
// SAFE - use these instead
element.textContent = userInput; // Escaped automatically
element.setAttribute('data-id', id); // Safe for attributes
document.createTextNode(userInput); // Creates safe text nodeURL Validation
function isSafeURL(url) {
try {
const parsed = new URL(url);
return ['http:', 'https:'].includes(parsed.protocol);
} catch {
return false;
}
}
// Usage
const href = isSafeURL(userURL) ? userURL : '#';Context-Specific Encoding
Different contexts require different encoding approaches:
- HTML Entity Encoding: Safest option for text content
- Attribute Encoding: For HTML attributes
- JavaScript Escaping: For script contexts
- URL Encoding: For URL parameters
- CSS Escaping: For stylesheet contexts
Always encode output by the specific context where data will be rendered.
Additional Implementations
See references/python-sanitization.md for:
- Python bleach library usage
- Flask/Django template escaping
- Server-side validation patterns
See references/nodejs-advanced.md for:
- Complete XSSPrevention class with all methods
- Express middleware (xssProtection)
- React components (SafeText, SafeHTML, SafeLink, useSanitizedInput)
- Helmet CSP configuration
Best Practices
✅ DO:
- Encode output by default
- Use templating engines with auto-escaping
- Implement CSP headers
- Sanitize rich content with allowlists
- Validate URLs with protocol whitelisting
- Use HTTPOnly cookies
- Conduct regular security testing
- Leverage secure frameworks
❌ DON'T:
- Trust user input
- Use unsafe functions (eval, innerHTML)
- Disable security features for convenience
- Rely solely on client-side validation
- Use blocklists instead of allowlists
- Skip context-specific encoding
- Allow arbitrary script execution
Security Checklist
- [ ] Encode all output by context (HTML, attribute, JS)
- [ ] Sanitize HTML with allowlist (not blocklist)
- [ ] Implement strict CSP headers
- [ ] Use HTTPOnly cookies for sessions
- [ ] Validate and sanitize URLs
- [ ] Avoid innerHTML with user content
- [ ] Regular security testing
Resources
Advanced Node.js XSS Prevention
Complete XSSPrevention Class
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)
};
}
}
module.exports = XSSPrevention;Express Middleware
// 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) {
// Handle null/undefined
if (obj === null || obj === undefined) {
return obj;
}
// Handle arrays - preserve array structure
if (Array.isArray(obj)) {
return obj.map(item => {
if (typeof item === 'string') {
return XSSPrevention.stripHTML(item);
} else if (typeof item === 'object' && item !== null) {
return sanitizeObject(item); // Recurse for nested objects/arrays
} else {
return item;
}
});
}
// Handle plain objects
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); // Recurse for nested objects/arrays
} 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 });
});React Components
// React XSS-safe components
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>
);
}
// Import the canonical sanitizeURL from XSSPrevention class
// NOTE: This uses the same implementation as XSSPrevention.sanitizeURL
// which allows 'http:', 'https:', and 'mailto:' protocols
function sanitizeURL(url) {
try {
const parsed = new URL(url);
// Consistent with XSSPrevention.sanitizeURL - allows mailto for email links
if (!['http:', 'https:', 'mailto:'].includes(parsed.protocol)) {
return '';
}
return parsed.href;
} catch {
return '';
}
}
// Alternative: Use XSSPrevention class directly for consistency
// import { XSSPrevention } from './xss-prevention';
// const sanitizeURL = XSSPrevention.sanitizeURL;
// 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 };Helmet CSP Configuration
const helmet = require('helmet');
const crypto = require('crypto');
// Generate nonce for inline scripts
function generateNonce() {
return crypto.randomBytes(16).toString('base64');
}
function setupCSP(app) {
// Generate nonce per-request BEFORE setting CSP header
app.use((req, res, next) => {
res.locals.nonce = generateNonce();
next();
});
// Use Helmet with dynamic nonce via function
app.use((req, res, next) => {
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
// Use dynamic nonce from res.locals
scriptSrc: [
"'self'",
`'nonce-${res.locals.nonce}'`, // Dynamic per-request nonce
"https://cdn.example.com"
],
// Styles with dynamic nonce
styleSrc: [
"'self'",
`'nonce-${res.locals.nonce}'`, // Dynamic per-request nonce
"https://fonts.googleapis.com"
],
// No inline styles/scripts without nonce
objectSrc: ["'none'"],
baseUri: ["'self'"],
// Report violations
reportUri: ['/api/csp-violations']
}
})(req, res, next);
});
// CSP violation reporter
app.post('/api/csp-violations', (req, res) => {
console.error('CSP Violation:', req.body);
res.status(204).end();
});
}
// Alternative: Custom CSP middleware without Helmet (more control)
function setupCSPCustom(app) {
app.use((req, res, next) => {
// Generate nonce
const nonce = generateNonce();
res.locals.nonce = nonce;
// Build CSP header with dynamic nonce
const cspHeader = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' https://cdn.example.com`,
`style-src 'self' 'nonce-${nonce}' https://fonts.googleapis.com`,
"object-src 'none'",
"base-uri 'self'",
"report-uri /api/csp-violations"
].join('; ');
res.setHeader('Content-Security-Policy', cspHeader);
next();
});
// CSP violation reporter
app.post('/api/csp-violations', (req, res) => {
console.error('CSP Violation:', req.body);
res.status(204).end();
});
}
// In templates: <script nonce="<%= nonce %>">
// The nonce in the header will match res.locals.noncePython XSS Prevention
Bleach Library
import bleach
# Basic sanitization
def sanitize_html(dirty_html):
allowed_tags = ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li']
allowed_attrs = {'a': ['href', 'title']}
return bleach.clean(
dirty_html,
tags=allowed_tags,
attributes=allowed_attrs,
strip=True
)
# Link sanitization
def sanitize_url(url):
return bleach.clean(url, tags=[], strip=True)
# Usage
user_content = '<script>alert("xss")</script><p>Hello <b>World</b></p>'
safe_content = sanitize_html(user_content)
# Result: '<p>Hello <b>World</b></p>'Flask Template Escaping
from flask import Flask, render_template, Markup
from markupsafe import escape
app = Flask(__name__)
@app.route('/profile/<username>')
def profile(username):
# Automatically escaped in templates
return render_template('profile.html', username=username)
@app.route('/comment', methods=['POST'])
def add_comment():
comment = request.form['comment']
# Manual escaping when needed
safe_comment = escape(comment)
# If you need to render trusted HTML
trusted_html = Markup('<b>Bold text</b>') # Only for trusted content!
return render_template('comment.html',
comment=safe_comment,
trusted=trusted_html)<!-- profile.html - Auto-escaped by Jinja2 -->
<h1>Welcome, {{ username }}</h1>
<!-- Explicitly mark as safe (DANGEROUS - only for sanitized content) -->
{{ sanitized_html | safe }}Django Template Security
from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe
import bleach
def clean_user_html(html_content):
"""Sanitize user HTML before storing"""
allowed_tags = ['p', 'br', 'b', 'i', 'em', 'strong', 'a']
allowed_attrs = {'a': ['href']}
return bleach.clean(
html_content,
tags=allowed_tags,
attributes=allowed_attrs,
strip=True
)
# In views
def comment_view(request):
if request.method == 'POST':
raw_content = request.POST['content']
safe_content = clean_user_html(raw_content)
Comment.objects.create(content=safe_content)<!-- Django template - auto-escaped by default -->
<p>{{ user_input }}</p>
<!-- For pre-sanitized content (like Comment.content), use |safe filter -->
<!-- Since clean_user_html() already sanitized it, safe to render -->
{% for comment in comments %}
<div class="comment">
{{ comment.content|safe }}
</div>
{% endfor %}
<!-- Alternative: use autoescape off for blocks of sanitized content -->
{% autoescape off %}
{{ already_sanitized_html }}
{% endautoescape %}Input Validation
import re
from urllib.parse import urlparse
def validate_url(url):
"""Validate URL is safe to use. Returns url if valid, None otherwise."""
try:
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
return None
if not parsed.netloc:
return None
return url
except Exception:
return None
def validate_username(username):
"""Validate alphanumeric username (3-30 chars). Returns username if valid, None otherwise."""
if not isinstance(username, str):
return None
if re.match(r'^[a-zA-Z0-9_]{3,30}$', username):
return username
return None
def sanitize_filename(filename):
"""Remove path traversal and unsafe characters. Returns sanitized filename or None if empty."""
if not isinstance(filename, str):
return None
sanitized = re.sub(r'[^\w\-.]', '', filename)
# Return None if sanitization resulted in empty string
return sanitized if sanitized else None
# Usage example
url = validate_url(user_input)
if url:
# Safe to use
redirect(url)
else:
# Invalid URL
return error("Invalid URL")
username = validate_username(user_input)
if username:
# Valid username
save_user(username)
else:
# Invalid username
return error("Invalid username format")CSP with Flask
from flask import Flask, make_response
import secrets
app = Flask(__name__)
@app.after_request
def add_security_headers(response):
nonce = secrets.token_urlsafe(16)
csp = "; ".join([
"default-src 'self'",
f"script-src 'self' 'nonce-{nonce}'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"frame-ancestors 'none'",
"base-uri 'self'"
])
response.headers['Content-Security-Policy'] = csp
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
return response