xss-prevention
SKILL.md
XSS Prevention (OWASP A07)
Prevent Cross-Site Scripting attacks by properly encoding output and sanitizing user input.
When to Use
- Displaying user-generated content
- Building dynamic HTML
- Implementing rich text editors
- Rendering markdown or HTML
- Working with URL parameters in pages
- Building search results pages
XSS Types
| Type | Vector | Example |
|---|---|---|
| Reflected | URL parameters | ?search=<script>alert(1)</script> |
| Stored | Database content | Comment with malicious script |
| DOM-based | Client-side JS | document.write(location.hash) |
Vulnerable Patterns
Server-Side
// VULNERABLE - Direct interpolation
app.get('/search', (req, res) => {
res.send(`<h1>Results for: ${req.query.q}</h1>`);
});
// VULNERABLE - Template without escaping
res.render('profile', { bio: user.bio }); // If template doesn't auto-escape
Client-Side
// VULNERABLE - innerHTML with user data
element.innerHTML = userInput;
document.getElementById('output').innerHTML = data;
// VULNERABLE - document.write
document.write(location.search);
// VULNERABLE - eval with user data
eval(userCode);
// VULNERABLE - jQuery html()
$('#output').html(userData);
// VULNERABLE - React dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{__html: userContent}} />
Secure Implementation
1. Output Encoding
// HTML entity encoding
function escapeHtml(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
};
return text.replace(/[&<>"'/]/g, char => map[char]);
}
// Usage
app.get('/search', (req, res) => {
const safeQuery = escapeHtml(req.query.q);
res.send(`<h1>Results for: ${safeQuery}</h1>`);
});
2. Context-Aware Encoding
// Different contexts need different encoding
const encoders = {
// HTML body context
html: (str) => str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>'),
// HTML attribute context
attr: (str) => str
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, '''),
// JavaScript string context
js: (str) => str
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n'),
// URL parameter context
url: (str) => encodeURIComponent(str),
// CSS context
css: (str) => str.replace(/[^a-zA-Z0-9]/g, char =>
'\\' + char.charCodeAt(0).toString(16) + ' '
)
};
// Usage based on context
`<div>${encoders.html(userInput)}</div>`
`<input value="${encoders.attr(userInput)}">`
`<script>var x = '${encoders.js(userInput)}';</script>`
`<a href="/search?q=${encoders.url(userInput)}">`
3. Content Security Policy (CSP)
// Express with helmet
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // No 'unsafe-inline'!
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"]
}
}));
// With nonce for inline scripts (when needed)
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64');
next();
});
app.use(helmet.contentSecurityPolicy({
directives: {
scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`]
}
}));
// In template
`<script nonce="${nonce}">...</script>`
4. HTML Sanitization
// Using DOMPurify (recommended)
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
function sanitizeHtml(dirty) {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'li'],
ALLOWED_ATTR: ['href', 'target'],
ALLOW_DATA_ATTR: false
});
}
// For rich text editors
function sanitizeRichText(html) {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: [
'h1', 'h2', 'h3', 'p', 'br', 'ul', 'ol', 'li',
'b', 'i', 'u', 'strong', 'em', 'a', 'img',
'blockquote', 'code', 'pre'
],
ALLOWED_ATTR: ['href', 'src', 'alt', 'class'],
FORBID_TAGS: ['script', 'style', 'iframe', 'form', 'input'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover']
});
}
5. React Safe Patterns
// SAFE - React auto-escapes by default
function UserProfile({ user }) {
return <div>{user.name}</div>; // Automatically escaped
}
// SAFE - Use textContent for DOM
useEffect(() => {
document.getElementById('output').textContent = userInput;
}, [userInput]);
// When you MUST render HTML, sanitize first
import DOMPurify from 'dompurify';
function RichContent({ html }) {
const sanitized = DOMPurify.sanitize(html);
return <div dangerouslySetInnerHTML={{__html: sanitized}} />;
}
// SAFE - URL validation
function SafeLink({ url, children }) {
const isValidUrl = url.startsWith('https://') || url.startsWith('/');
if (!isValidUrl) {
return <span>{children}</span>;
}
return <a href={url}>{children}</a>;
}
6. Vue.js Safe Patterns
<!-- SAFE - Vue auto-escapes -->
<template>
<div>{{ userInput }}</div>
</template>
<!-- DANGEROUS - v-html -->
<template>
<div v-html="userContent"></div> <!-- XSS risk! -->
</template>
<!-- SAFE - Sanitize before v-html -->
<script>
import DOMPurify from 'dompurify';
export default {
computed: {
safeContent() {
return DOMPurify.sanitize(this.userContent);
}
}
}
</script>
<template>
<div v-html="safeContent"></div>
</template>
7. URL Validation
// Prevent javascript: URLs
function isSafeUrl(url) {
try {
const parsed = new URL(url, window.location.origin);
return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
} catch {
return false;
}
}
// Usage
function SafeAnchor({ href, children }) {
if (!isSafeUrl(href)) {
return <span>{children}</span>;
}
return <a href={href} rel="noopener noreferrer">{children}</a>;
}
Framework-Specific Guidance
Express/Node.js
- Use template engines with auto-escaping (EJS, Pug, Handlebars)
- Set
{escape: true}in template options - Use helmet for CSP headers
Django
- Templates auto-escape by default
- Use
|safefilter only with sanitized content - Enable CSP middleware
Rails
- ERB auto-escapes with
<%= %> - Use
raw()orhtml_safeonly with sanitized content - Use
sanitize()helper for user HTML
Laravel
- Blade
{{ }}auto-escapes - Use
{!! !!}only with sanitized content - Use
e()helper for manual escaping
Code Review Checklist
- All user input encoded before output
- Context-appropriate encoding used
- No innerHTML with unsanitized data
- No dangerouslySetInnerHTML without sanitization
- No eval() or new Function() with user data
- CSP headers configured
- URL schemes validated (no javascript:)
- Rich text sanitized with DOMPurify or similar
- Template engine auto-escaping enabled
- No user data in script blocks
Testing for XSS
<!-- Test payloads -->
<script>alert('XSS')</script>
<img src=x onerror=alert('XSS')>
<svg onload=alert('XSS')>
<body onload=alert('XSS')>
"><script>alert('XSS')</script>
'-alert('XSS')-'
javascript:alert('XSS')
data:text/html,<script>alert('XSS')</script>
# Automated scanning
# Use Burp Suite, OWASP ZAP, or XSStrike
python xsstrike.py -u "http://target.com/search?q=test"
Best Practices
- Encode Output: Always encode data based on context
- Use CSP: Defense in depth against XSS
- Sanitize HTML: Use DOMPurify for user HTML
- Validate URLs: Block javascript: and data: URLs
- HttpOnly Cookies: Prevent cookie theft
- Use Frameworks: Leverage auto-escaping features
Weekly Installs
2
Repository
latestaiagents/…t-skillsGitHub Stars
2
First Seen
Feb 4, 2026
Installed on
mcpjam2
claude-code2
replit2
junie2
windsurf2
zencoder2