XSS Prevention Cheat Sheet
A complete reference for defending against Cross-Site Scripting attacks across every output context, framework, and deployment pattern.
Understanding Cross-Site Scripting (XSS)
Cross-Site Scripting is a client-side injection vulnerability that occurs when an application includes untrusted data in its web output without proper validation or encoding. An attacker exploits XSS to execute arbitrary JavaScript in a victim's browser, enabling session hijacking, credential theft, keylogging, defacement, and redirection to malicious sites. XSS consistently ranks in the OWASP Top 10 and remains one of the most prevalent vulnerabilities on the web.
Types of XSS Attacks
Stored XSS (Persistent): The malicious script is permanently stored on the target server, typically in a database, forum post, comment field, or user profile. Every user who views the affected content executes the payload. This is the most dangerous variant because it requires no victim interaction beyond visiting the page.
Reflected XSS (Non-Persistent): The injected script is reflected off the web server in an error message, search result, or URL parameter. The attack requires the victim to click a crafted link. While less severe than stored XSS, reflected XSS is the most common type and is frequently used in phishing campaigns.
DOM-Based XSS: The vulnerability exists in client-side JavaScript rather than server-side code. The malicious payload is processed by the DOM environment in the victim's browser. Common DOM XSS sinks include document.write(), innerHTML, eval(), and location.href. DOM XSS is particularly difficult to detect with server-side scanners because the payload never reaches the server.
OWASP Prevention Rules
The OWASP XSS Prevention Cheat Sheet defines seven rules that form the foundation of XSS defense:
Rule 0 — Never insert untrusted data except in allowed locations. Do not place user input directly into script blocks, HTML comments, attribute names, tag names, or CSS. These contexts have no safe encoding scheme.
Rule 1 — HTML-encode before inserting into element content. Convert &, <, >, ", ', and / to their HTML entity equivalents when inserting data between HTML tags.
Rule 2 — Attribute-encode before inserting into attributes. Encode all characters with ASCII values less than 256 that are not alphanumeric. Always quote attribute values to prevent attribute injection.
Rule 3 — JavaScript-encode before inserting into JavaScript data values. Encode all non-alphanumeric characters with the \xHH format. Never place user data inside eval() or event handlers without encoding.
Rule 4 — CSS-encode before inserting into CSS property values. Use the \HH format for any character that is not alphanumeric. Never allow user data in CSS expressions or url() values.
Rule 5 — URL-encode before inserting into URL parameter values. Use percent-encoding for all untrusted data in URL parameters. Validate that URLs begin with http:// or https:// to block javascript: and data: URLs.
Rule 6 — Use an HTML sanitization library for rich content. When you must accept HTML input (rich text editors, markdown), use a well-maintained sanitizer like DOMPurify, Bleach, or the OWASP Java HTML Sanitizer. Never write your own HTML sanitizer.
Output Encoding by Context
The critical insight in XSS prevention is that encoding must match the output context. Data that is safe in an HTML body is not safe in a JavaScript string, URL parameter, or CSS value. Each context has its own encoding rules:
HTML body context: Use HTML entity encoding. Libraries: he (Node.js), html.escape() (Python), htmlspecialchars() (PHP), StringEscapeUtils.escapeHtml4() (Java).
HTML attribute context: Use attribute encoding with quoted attributes. Never use unquoted attributes with user data.
JavaScript context: Use JavaScript string encoding (\xHH). Prefer JSON.stringify() for embedding data in JavaScript. Never concatenate user data into script blocks.
URL context: Use encodeURIComponent() for parameter values. Validate the URL scheme to block javascript: and data: URIs.
CSS context: Use CSS hex encoding. Better yet, avoid placing user data in CSS entirely.
Content Security Policy as a Defense Layer
CSP is not a replacement for output encoding, but it is a powerful defense-in-depth layer that mitigates XSS impact even when encoding failures occur. A strict CSP configuration blocks inline script execution and restricts script sources to trusted origins. The recommended approach is nonce-based CSP:
Content-Security-Policy: script-src 'nonce-{random}' 'strict-dynamic'; object-src 'none'; base-uri 'self';
Generate a cryptographically random nonce per response and include it on every legitimate <script> tag. This allows your scripts to execute while blocking injected scripts that lack the nonce.
Framework-Specific Protections
React: JSX escapes embedded values by default. Avoid dangerouslySetInnerHTML. Validate href attributes to block javascript: URLs. Use DOMPurify when you must render HTML.
Vue: Double-brace interpolation ({{ }}) auto-escapes. The v-html directive does NOT escape and is a common XSS vector. Never bind user data to v-html without sanitization.
Angular: Angular sanitizes values by default in templates. The bypassSecurityTrustHtml() method disables sanitization and should only be used with trusted content. Angular's built-in sanitizer is robust but can be bypassed with prototype pollution.
Server-side templates (EJS, Jinja2, Blade): Use the auto-escaping syntax by default (<%= %> in EJS, {{ }} in Jinja2 with autoescape, {{ }} in Blade). Avoid raw output syntax (<%- %>, |safe, {!! !!}) unless the content has been sanitized.