DOMPurify.sanitize(userInput) for client-side HTML sanitization; framework auto-escaping (React JSX, Django templates, Go html/template) for server-side.dangerouslySetInnerHTML (React), v-html (Vue), [innerHTML] (Angular), and raw innerHTML bypass framework auto-escaping and reintroduce XSS risk.| # | XSS Type | Vector | Impact | Prevention |
|---|---|---|---|---|
| 1 | Reflected XSS | Malicious URL parameter reflected in page response | Session hijacking, phishing | HTML-encode all reflected output; validate input on arrival |
| 2 | Stored XSS | Malicious payload saved to DB and rendered to other users | Account takeover, worm propagation | Encode on output; sanitize HTML if rich text needed |
| 3 | DOM-based XSS | Client-side JS reads attacker-controlled source and passes to dangerous sink | Full client-side compromise | Avoid dangerous sinks; use textContent; use DOMPurify |
| 4 | Mutation XSS (mXSS) | Browser HTML parser mutates sanitized markup into executable form | Bypasses sanitizers | Keep DOMPurify updated; use Trusted Types API |
| 5 | Blind XSS | Stored payload executes in admin panel or internal tool | Internal system compromise | Encode all output in admin views; monitor with XSS Hunter |
| # | Output Context | Encoding Rule | Safe Function | Dangerous Pattern |
|---|---|---|---|---|
| 1 | HTML body | HTML entity encode & < > " ' | textContent, framework auto-escape | innerHTML = userInput |
| 2 | HTML attribute | HTML entity encode; always quote attributes | setAttribute(name, val) | <div title= + userInput + > |
| 3 | JavaScript string | Unicode-escape \uXXXX or \xHH format | JSON.stringify() into script context | var x = ' + userInput + ' |
| 4 | URL parameter | Percent-encode %HH | encodeURIComponent() | href=" + userInput + " |
| 5 | URL scheme | Allowlist http/https only | Validate protocol before use | href= + userInput (allows javascript:) |
| 6 | CSS value | CSS hex encode \XX; allowlist values | CSS custom properties with validated input | style="color: + userInput + " |
| 7 | JSON in HTML | JSON.stringify + HTML entity encode | Django |json_script, Jinja2 |tojson | <script>var x = ${userInput}</script> |
START: Where does user input appear in your output?
├── HTML body text?
│ ├── YES → Use framework auto-escaping (React JSX, Django {{ }}, Go html/template)
│ └── NO ↓
├── HTML attribute value?
│ ├── YES → HTML-encode value AND always quote the attribute
│ └── NO ↓
├── Inside <script> tag or JavaScript string?
│ ├── YES → Use JSON.stringify() + HTML entity encode, or pass via data attributes
│ └── NO ↓
├── Inside a URL (href, src)?
│ ├── YES → Validate protocol is http/https, then encodeURIComponent() for params
│ └── NO ↓
├── Inside CSS (style attribute)?
│ ├── YES → Allowlist valid CSS values; never interpolate user input directly
│ └── NO ↓
├── User needs to submit rich HTML content?
│ ├── YES → Sanitize with DOMPurify server-side AND client-side before rendering
│ └── NO ↓
└── DEFAULT → HTML-encode all output + deploy strict CSP as defense-in-depth
CSP prevents inline script execution even if an XSS payload bypasses encoding. Use nonce-based CSP for the strongest protection. [src3]
Content-Security-Policy:
default-src 'self';
script-src 'nonce-{random}' 'strict-dynamic';
style-src 'self' 'nonce-{random}';
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
Verify: Open browser DevTools > Console -- CSP violations appear as errors. Check report-uri endpoint for reports.
Use your framework's built-in auto-escaping. Every major framework escapes by default when used correctly. [src1]
// React: JSX auto-escapes by default
function UserGreeting({ name }) {
return <h1>Hello, {name}</h1>;
}
Verify: Inject <script>alert(1)</script> as user input -- it should render as visible text, not execute.
When you must accept HTML from users (comments, WYSIWYG editors), sanitize with DOMPurify. [src5]
import DOMPurify from 'dompurify'; // v3.x
const cleanHTML = DOMPurify.sanitize(userInput, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href', 'title'],
ALLOW_DATA_ATTR: false
});
Verify: Test with payload <img src=x onerror=alert(1)> -- should be stripped.
Prevent javascript: and data: URL injection in href and src attributes. [src2]
function isSafeUrl(url) {
try {
const parsed = new URL(url, window.location.origin);
return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
} catch { return false; }
}
Verify: Test with javascript:alert(1) -- should be rejected.
Trusted Types prevent DOM XSS by requiring typed objects for dangerous sinks. [src7]
Content-Security-Policy: require-trusted-types-for 'script'
Verify: Setting innerHTML with a raw string will throw a TypeError when Trusted Types is enforced.
import DOMPurify from 'dompurify'; // ^3.0.0
// SAFE: React auto-escapes expressions in JSX
function Comment({ author, text }) {
return (
<div className="comment">
<strong>{author}</strong>
<p>{text}</p>
</div>
);
}
// When rich HTML is required, sanitize first
function RichComment({ htmlContent }) {
const clean = DOMPurify.sanitize(htmlContent);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
const express = require('express'); // ^4.18.0
const helmet = require('helmet'); // ^7.1.0
const crypto = require('crypto');
const app = express();
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
next();
});
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: [(req, res) => `'nonce-${res.locals.cspNonce}'`],
objectSrc: ["'none'"],
baseUri: ["'none'"]
}
}
}));
# Django auto-escapes all template variables by default
from django.utils.html import escape
def profile(request, username):
return render(request, 'profile.html', {'username': username})
# {{ username }} is auto-escaped in the template
# DANGEROUS: {{ user_input|safe }} -- NEVER use with untrusted data
# For JSON in templates: {{ data|json_script:"user-data" }}
package main
import (
"html/template" // NOT text/template
"net/http"
)
var tmpl = template.Must(template.New("page").Parse(`
<h1>Hello, {{.Name}}</h1>
<a href="{{.URL}}">Profile</a>
`))
func handler(w http.ResponseWriter, r *http.Request) {
data := struct{ Name, URL string }{
Name: r.URL.Query().Get("name"),
URL: "/users/" + r.URL.Query().Get("id"),
}
tmpl.Execute(w, data)
}
// BAD -- blacklist filtering is trivially bypassed
function sanitize(input) {
return input
.replace(/<script>/gi, '')
.replace(/onerror/gi, '');
}
// Bypass: <scr<script>ipt>, <img src=x OnError=alert(1)>
// GOOD -- encode based on output context
function escapeHtml(str) {
const map = {'&':'&','<':'<','>':'>','"':'"',"'":'''};
return String(str).replace(/[&<>"']/g, c => map[c]);
}
<!-- BAD -- HTML encoding inside a JavaScript context does NOT prevent XSS -->
<script>
var name = '{{html_escaped_user_input}}';
</script>
<!-- GOOD -- use data attributes instead -->
<div id="user" data-name="{{html_escaped_user_input}}"></div>
<script>
var name = document.getElementById('user').dataset.name;
</script>
# BAD -- unsafe-inline defeats CSP's XSS protection
Content-Security-Policy: script-src 'self' 'unsafe-inline'
# GOOD -- nonce-based CSP blocks injected inline scripts
Content-Security-Policy: script-src 'nonce-abc123' 'strict-dynamic'
// BAD -- direct DOM injection of user input
document.getElementById('output').innerHTML = userInput;
// GOOD -- textContent for plain text
document.getElementById('output').textContent = userInput;
// GOOD -- DOMPurify when HTML rendering is required
import DOMPurify from 'dompurify';
document.getElementById('output').innerHTML = DOMPurify.sanitize(userInput);
<script> blocks without escaping allows </script> injection. Fix: Use JSON.stringify() and replace </ with <\/, or use Django's |json_script filter. [src1]<a href="{{user_url}}"> allows javascript:alert(1) even with HTML encoding. Fix: Validate URL protocol is http: or https: before use. [src2]<script> tags and event handlers. Fix: Sanitize SVGs with DOMPurify (supports SVG mode) or serve with restrictive CSP. [src5]eval(`Hello ${name}`) enables code execution. Fix: Never use eval(), new Function(), or setTimeout(string) with user data. [src6]window.addEventListener('message', handler) without verifying event.origin. Fix: Always check event.origin against an allowlist. [src6]&lt; instead of <. Fix: Encode exactly once, at the point of output. [src2]# Check current CSP headers of a site
curl -sI https://your-site.com | grep -i content-security-policy
# Scan for XSS with OWASP ZAP (Docker)
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \
-t https://your-site.com -r report.html
# Find dangerous sinks in JavaScript codebase
grep -rn 'innerHTML\|outerHTML\|document\.write\|eval(' --include="*.js" .
# Find React dangerouslySetInnerHTML usage
grep -rn 'dangerouslySetInnerHTML' --include="*.jsx" --include="*.tsx" .
# Find Vue v-html usage
grep -rn 'v-html' --include="*.vue" .
# Validate CSP: visit https://csp-evaluator.withgoogle.com/
| Standard/Tool | Version | Status | Key Feature |
|---|---|---|---|
| CSP Level 3 | W3C CR | Current | strict-dynamic, nonce-based policies |
| CSP Level 2 | W3C Rec | Supported | script-src, style-src, report-uri |
| Trusted Types | Draft | Chrome 83+, Firefox behind flag | DOM sink protection |
| DOMPurify | 3.x | Current | Trusted Types support, ES module |
| DOMPurify | 2.x | Maintained | CommonJS, UMD bundles |
| Sanitizer API | Draft | Chrome 105+ (origin trial) | Native browser HTML sanitization |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Any web app renders user input in HTML | Static site with zero user input | No XSS prevention needed |
| Building WYSIWYG editor or comment system with rich text | You can use plain text only (no HTML rendering) | textContent + framework auto-escaping only |
| CSP is your only server-level control | You assume WAF blocks all XSS | Output encoding at the application level |
| Third-party content is embedded (ads, widgets) | Content from fully trusted internal sources only | Minimal CSP + auto-escaping may suffice |