From latestaiagents
OWASP A07 - Cross-Site Scripting (XSS) Prevention. Use this skill when rendering user input in HTML, handling DOM manipulation, or building frontend components. Activate when: XSS, cross-site scripting, user input display, innerHTML, dangerouslySetInnerHTML, template injection, script injection, sanitize HTML, escape output.
npx claudepluginhub latestaiagents/agent-skills --plugin skills-authoringThis skill uses the workspace's default tool permissions.
**Prevent Cross-Site Scripting attacks by properly encoding output and sanitizing user input.**
Prevents XSS attacks via input sanitization, output encoding, CSP headers, DOMPurify, and safe DOM APIs. Use for user-generated content, rich text editors, comments, and dynamic HTML.
Prevents XSS attacks via input sanitization, output encoding, CSP headers, and secure practices. Guides Node.js, Python, React implementations for user-generated content like comments and rich editors.
Prevents XSS attacks by encoding output, sanitizing HTML with DOMPurify, setting CSP headers via Helmet/nonces, and avoiding dangerous sinks in React, Express, and NestJS apps handling user content.
Share bugs, ideas, or general feedback.
Prevent Cross-Site Scripting attacks by properly encoding output and sanitizing user input.
| 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 - 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
// 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}} />
// 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>`);
});
// 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)}">`
// 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>`
// 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']
});
}
// 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>;
}
<!-- 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>
// 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>;
}
{escape: true} in template options|safe filter only with sanitized content<%= %>raw() or html_safe only with sanitized contentsanitize() helper for user HTML{{ }} auto-escapes{!! !!} only with sanitized contente() helper for manual escaping<!-- 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"