XSS Prevention
Prevent cross-site scripting (XSS) attacks through output encoding, input sanitization, and secure rendering practices.
You are an expert in preventing cross-site scripting (XSS) vulnerabilities in web applications across frontend and backend stacks. ## Key Points - **Reflected XSS**: Malicious input is immediately reflected back in the HTTP response (e.g., search results echoing the query). - **Stored XSS**: Malicious input is persisted (database, file) and later rendered to other users (e.g., forum posts, comments). - **DOM-based XSS**: The vulnerability exists entirely in client-side JavaScript that processes untrusted data and writes it into the DOM. 1. **Default to encoding**: Use frameworks that auto-escape output (React, Angular, Jinja2 with autoescape). Only bypass when absolutely necessary and after sanitization. 2. **Apply context-specific encoding**: HTML body, attributes, JavaScript, URLs, and CSS each need different encoding. 3. **Use Content-Security-Policy headers**: CSP provides a second layer of defense even if XSS encoding is missed. See the `content-security-policy` skill. 4. **Sanitize on output, not just input**: Data flows change over time. Encode at the point of rendering, not only at the point of storage. 5. **Avoid dangerous APIs**: `innerHTML`, `document.write()`, `eval()`, `v-html`, `dangerouslySetInnerHTML` — avoid or gate behind sanitization. 6. **Set the `HttpOnly` flag on session cookies**: This prevents JavaScript from reading cookies even if XSS occurs. 7. **Use a strict DOMPurify config**: Allowlist tags and attributes rather than blocklisting. 8. **Enable Trusted Types**: Use the Trusted Types API to enforce sanitization at the browser level. - **Assuming a framework auto-escapes everywhere**: React escapes JSX but not `dangerouslySetInnerHTML`. Angular escapes templates but not `bypassSecurityTrustHtml`. Always verify context.
skilldb get security-practices-skills/XSS PreventionFull skill: 195 linesXSS Prevention — Application Security
You are an expert in preventing cross-site scripting (XSS) vulnerabilities in web applications across frontend and backend stacks.
Core Philosophy
XSS prevention is fundamentally about maintaining the boundary between data and code in the browser. When a web application renders user-supplied content, the browser must be able to distinguish between markup the developer intended and markup an attacker injected. Every XSS vulnerability is a failure of this boundary — a place where untrusted data was interpreted as executable code.
The correct mental model is "encode on output, not on input." Data should be stored in its canonical form and encoded for the specific rendering context at the moment it is displayed. HTML body, HTML attributes, JavaScript strings, URLs, and CSS values each require different encoding strategies. A value that is safe in one context can be dangerous in another, which is why a single sanitization pass at input time is insufficient. The rendering context determines the defense, and that context is only known at output time.
Modern frameworks like React, Angular, and Vue have made XSS significantly harder to introduce by auto-escaping template expressions. But this safety creates a dangerous complacency. Every framework provides escape hatches — dangerouslySetInnerHTML, bypassSecurityTrustHtml, v-html — and developers reach for them when they need to render rich content. These escape hatches must be treated as security-critical code paths that require explicit sanitization with a library like DOMPurify and careful review. The framework's default safety is only as strong as the discipline around its exceptions.
Anti-Patterns
-
Using
innerHTMLordangerouslySetInnerHTMLwith unsanitized content: These APIs bypass the framework's automatic escaping and inject raw HTML into the DOM. Any user-controlled data passed through them without sanitization by DOMPurify or equivalent is a direct XSS vector. -
Sanitizing on input and trusting the stored value forever: Data that was sanitized when it entered the system may be rendered in a new context months later — an API response consumed by a different frontend, a mobile app, or an email template. Encoding at the point of output is the only reliable defense.
-
Relying on blocklist-based sanitization: Stripping
<script>tags or filteringonerrorattributes is a losing game. Attackers continuously discover new tags, attributes, and encoding tricks that bypass blocklists. Use an allowlist-based sanitizer that permits only known-safe elements and attributes. -
Interpolating user data into inline
<script>blocks: Even with HTML entity encoding, placing user input inside a<script>tag creates complex parsing interactions that frequently lead to injection. Pass data to JavaScript viadata-attributes or a JSON blob with proper encoding, never through template interpolation inside script elements. -
Assuming framework auto-escaping covers all contexts: React escapes JSX expressions rendered as text content, but it does not protect
hrefattributes fromjavascript:URIs, and it cannot help if you concatenate user input into a string that gets passed toeval()ornew Function(). Each context needs its own defense.
Overview
Cross-site scripting (XSS) occurs when an attacker injects malicious scripts into content that is then served to other users. XSS can lead to session hijacking, credential theft, defacement, and malware distribution. The three main categories are Reflected XSS, Stored XSS, and DOM-based XSS.
Core Concepts
Types of XSS
- Reflected XSS: Malicious input is immediately reflected back in the HTTP response (e.g., search results echoing the query).
- Stored XSS: Malicious input is persisted (database, file) and later rendered to other users (e.g., forum posts, comments).
- DOM-based XSS: The vulnerability exists entirely in client-side JavaScript that processes untrusted data and writes it into the DOM.
Output Encoding Contexts
Different contexts require different encoding strategies:
| Context | Encoding Required |
|---|---|
| HTML body | HTML entity encoding |
| HTML attributes | Attribute encoding |
| JavaScript | JavaScript hex encoding |
| URL parameters | Percent/URL encoding |
| CSS values | CSS hex encoding |
Trusted Types API
The Trusted Types browser API prevents DOM XSS by requiring typed objects for dangerous sink assignments rather than raw strings.
Implementation Patterns
Context-Aware Output Encoding (Node.js / Express)
const he = require('he');
// HTML context — encode entities
function renderUserComment(comment) {
return `<div class="comment">${he.encode(comment)}</div>`;
}
// Attribute context — encode and quote
function renderLink(url, label) {
const safeUrl = encodeURI(url);
const safeLabel = he.encode(label);
return `<a href="${safeUrl}">${safeLabel}</a>`;
}
// Never interpolate into script blocks
// BAD: <script>var name = "${userName}";</script>
// GOOD: pass data via data attributes or JSON with encoding
function renderDataForScript(data) {
const encoded = JSON.stringify(data)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026');
return `<script>var config = ${encoded};</script>`;
}
React — Safe by Default, Watch the Escapes
// React auto-escapes in JSX expressions — this is safe
function UserGreeting({ name }) {
return <h1>Hello, {name}</h1>;
}
// DANGEROUS: dangerouslySetInnerHTML bypasses React's encoding
// Only use with sanitized content
import DOMPurify from 'dompurify';
function RichContent({ html }) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'ul', 'li'],
ALLOWED_ATTR: ['href', 'target'],
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
DOMPurify for User-Generated HTML
import DOMPurify from 'dompurify';
// Basic sanitization
const dirty = '<img src=x onerror=alert(1)><b>Hello</b>';
const clean = DOMPurify.sanitize(dirty);
// Result: '<b>Hello</b>'
// Strict config for comments
const commentConfig = {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'code', 'pre', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href'],
ALLOW_DATA_ATTR: false,
ADD_ATTR: ['target'],
FORBID_TAGS: ['style', 'script', 'iframe', 'form', 'object', 'embed'],
};
function sanitizeComment(rawHtml) {
return DOMPurify.sanitize(rawHtml, commentConfig);
}
DOM-Based XSS Prevention
// BAD — direct DOM manipulation with user input
document.getElementById('output').innerHTML = location.hash.slice(1);
// GOOD — use textContent for plain text
document.getElementById('output').textContent = location.hash.slice(1);
// GOOD — use Trusted Types policy
if (window.trustedTypes && trustedTypes.createPolicy) {
const escapePolicy = trustedTypes.createPolicy('escapePolicy', {
createHTML: (input) => DOMPurify.sanitize(input),
});
element.innerHTML = escapePolicy.createHTML(userInput);
}
Server-Side Template Engines
# Jinja2 (Python/Flask) — auto-escaping enabled by default
# In app config:
app.jinja_env.autoescape = True
# Template — this is auto-escaped:
# <p>{{ user_comment }}</p>
# If you MUST render raw HTML (pre-sanitized only):
# <div>{{ sanitized_html | safe }}</div>
# Always sanitize before marking safe:
import bleach
def sanitize_for_template(raw_html):
return bleach.clean(
raw_html,
tags=['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'li'],
attributes={'a': ['href']},
strip=True
)
Best Practices
- Default to encoding: Use frameworks that auto-escape output (React, Angular, Jinja2 with autoescape). Only bypass when absolutely necessary and after sanitization.
- Apply context-specific encoding: HTML body, attributes, JavaScript, URLs, and CSS each need different encoding.
- Use Content-Security-Policy headers: CSP provides a second layer of defense even if XSS encoding is missed. See the
content-security-policyskill. - Sanitize on output, not just input: Data flows change over time. Encode at the point of rendering, not only at the point of storage.
- Avoid dangerous APIs:
innerHTML,document.write(),eval(),v-html,dangerouslySetInnerHTML— avoid or gate behind sanitization. - Set the
HttpOnlyflag on session cookies: This prevents JavaScript from reading cookies even if XSS occurs. - Use a strict DOMPurify config: Allowlist tags and attributes rather than blocklisting.
- Enable Trusted Types: Use the Trusted Types API to enforce sanitization at the browser level.
Common Pitfalls
- Assuming a framework auto-escapes everywhere: React escapes JSX but not
dangerouslySetInnerHTML. Angular escapes templates but notbypassSecurityTrustHtml. Always verify context. - Encoding for the wrong context: HTML-encoding a value placed inside a
<script>tag does not prevent XSS. Match the encoding to the context. - Sanitizing only on input: Stored data may be rendered in new contexts later. Always encode at the point of output.
- Allowing
javascript:URIs: Check thathrefandsrcattributes only accepthttps:or relative URLs. Sanitizers may miss protocol-level attacks. - Using blocklists instead of allowlists: Attackers constantly find new bypasses. Always allowlist permitted tags, attributes, and protocols.
- Forgetting HTTP response headers: Without
Content-Type: text/html; charset=utf-8, browsers may sniff content and render injected scripts.
Install this skill directly: skilldb add security-practices-skills
Related Skills
Content Security Policy
Configure Content-Security-Policy headers to mitigate XSS, data injection, and clickjacking attacks.
CORS Security
Configure CORS headers correctly to control cross-origin resource access while preventing overly permissive policies.
CSRF Protection
Protect web applications against cross-site request forgery (CSRF) using tokens, SameSite cookies, and origin validation.
Input Validation
Validate and sanitize all user input at application boundaries using schemas, type coercion, and allowlists.
Secrets Management
Securely store, access, rotate, and audit application secrets and credentials using vaults, environment variables, and CI/CD integrations.
SQL Injection
Prevent SQL injection attacks using parameterized queries, ORM best practices, and input validation layers.