XSS (CWE-79, OWASP A03 Injection) lets attackers inject JavaScript into pages viewed by other users — stealing session cookies, capturing keystrokes, redirecting to phishing pages, or silently performing actions as the victim. Stored XSS, where the payload is saved in a database and rendered to every visitor, can compromise thousands of accounts from a single submission. NIST 800-53 SI-10 requires input sanitization. React's JSX auto-escaping prevents most XSS, but dangerouslySetInnerHTML with unsanitized database content bypasses the protection entirely. A comment field or profile bio rendered unsanitized is all an attacker needs.
High because stored XSS turns every page view into a potential account takeover — a single injected payload runs in every victim's browser session without further attacker interaction.
Sanitize user-generated HTML with DOMPurify before passing it to dangerouslySetInnerHTML. Better: avoid dangerouslySetInnerHTML entirely by using react-markdown for user content.
When you must render raw HTML:
import sanitizeHtml from 'sanitize-html'
const clean = sanitizeHtml(userHtml, {
allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'li'],
allowedAttributes: { a: ['href', 'rel'] },
allowedSchemes: ['http', 'https', 'mailto'],
})
// Then render:
<div dangerouslySetInnerHTML={{ __html: clean }} />
Search the codebase for dangerouslySetInnerHTML, innerHTML =, and v-html and audit each one. Any that render database-sourced content must sanitize first.
ID: security-hardening.input-validation.xss-prevention
Severity: high
What to look for: Count every location where user-supplied data is rendered in HTML output. For each, classify the escaping method: (a) framework auto-escaping, (b) manual escaping, (c) dangerouslySetInnerHTML/v-html, or (d) unescaped. check for dangerous DOM mutation patterns. Search for dangerouslySetInnerHTML, innerHTML =, document.write, eval(), new Function(), setTimeout(string), setInterval(string). Also check for server-side template injection patterns in non-React/JSX frameworks. For content rendered from user input or a database (user profiles, comments, rich text), verify that sanitization is applied before rendering.
Detector snippet (shell-capable tools only): If rg is available, list every source file that invokes a risky DOM-mutation API. Exit 0 with output means at least one risky call site exists — proceed to prose to classify each as sanitized or not. Exit 1 with no output means no risky call sites — likely pass. Exit >=2 — fall back to prose reasoning.
rg -l -e "dangerouslySetInnerHTML" -e "innerHTML\s*=" -e "document\.write\(" -e "new Function\(" src/
Pass criteria: No dangerouslySetInnerHTML with unsanitized user content. Dynamic content rendered via React/JSX (auto-escapes). When rich HTML must be rendered, a sanitization library (DOMPurify, sanitize-html) is applied before rendering. No innerHTML assignments with user content — 100% of render points must use proper escaping. Report: "X user-data render points found, all Y properly escaped."
Fail criteria: dangerouslySetInnerHTML={{ __html: userContent }} without sanitization. Direct innerHTML assignment with user-controlled content. eval() or new Function() with user-controlled strings.
Skip (N/A) when: The project renders no user-generated content — it is a static marketing site or dashboard with only data the current user themselves has entered.
Do NOT pass when: dangerouslySetInnerHTML or v-html is used with unsanitized user input, even if other render points are safe.
Detail on fail: Name the specific location. Example: "dangerouslySetInnerHTML in components/CommentBody.tsx renders post content from database without sanitization — stored XSS possible if content was stored unsanitized" or "innerHTML assignment in lib/notifications.js line 22 embeds user-supplied message"
Remediation: When you must render HTML from user content, sanitize it first:
import DOMPurify from 'dompurify'
// In a browser environment:
const clean = DOMPurify.sanitize(userHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'rel'],
})
// Server-side (Node.js) with sanitize-html:
import sanitizeHtml from 'sanitize-html'
const clean = sanitizeHtml(userHtml, {
allowedTags: sanitizeHtml.defaults.allowedTags,
allowedAttributes: { a: ['href', 'rel'] },
})
// Then render safely:
<div dangerouslySetInnerHTML={{ __html: clean }} />
Prefer avoiding dangerouslySetInnerHTML entirely — use a markdown renderer like react-markdown instead for user-authored content.