Skip to main content

No dangerouslySetInnerHTML with user input

ab-002573 · project-snapshot.injection.no-dangerous-html-from-input
Severity: highactive

Why it matters

dangerouslySetInnerHTML bypasses React's default XSS protection — anything passed through it renders as live HTML, including <script> tags, event handlers, and <iframe> exploits. When the HTML source traces back to user input, fetched data, search params, or a markdown parser that permits raw HTML, an attacker who controls that input controls script execution in every viewer's browser: session theft, token exfiltration, UI takeover, all in-scope. AI coding tools reach for dangerouslySetInnerHTML whenever a task mentions "render markdown" or "show rich text" — and because React flagged the API with a scary name but not a sanitizer, tools treat the name as the safety measure and forget DOMPurify entirely. The pattern is the single most common XSS vector in React codebases.

Severity rationale

High because the attack is stored-XSS-grade when the input is user-controlled, but dependent on an attacker actually reaching that field with hostile input — not an automatic compromise like a leaked key.

Remediation

Pipe the HTML through a sanitizer before rendering:

import DOMPurify from 'isomorphic-dompurify'
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(post.body) }} />

Deeper remediation guidance and cross-reference coverage for this check lives in the security-hardening Pro audit — run that after applying this fix for a more exhaustive pass on the same topic.

Detection

  • ID: project-snapshot.injection.no-dangerous-html-from-input
  • Severity: high
  • What to look for: Enumerate every dangerouslySetInnerHTML={{ __html: X }} in JSX/TSX files. For each, trace the source of X. Classify each as: (a) static literal/template with no interpolation, (b) constant from a trusted source (e.g., a sanitized markdown render via DOMPurify or sanitize-html), or (c) directly from props, state, fetch, or searchParams. Count each category.
  • Pass criteria: Zero usages in category (c). All usages are static literals or come through a documented sanitizer call.
  • Fail criteria: At least one usage where the HTML source is untrusted (props, fetched data, search params, form input) without a sanitizer in the call chain.
  • Skip (N/A) when: Framework is not React-based (Vue uses v-html; Svelte uses {@html} — those are different patterns). Note the framework when skipping.
  • Do NOT pass when: A markdown library is used but its output is rendered via dangerouslySetInnerHTML without an explicit sanitizer step — many markdown parsers allow raw HTML through by default.
  • Before evaluating, quote: Quote the JSX line for any matched usage, then describe how the HTML value is computed.
  • Report even on pass: "Found N dangerouslySetInnerHTML usages; all N use static or sanitized content."
  • Detail on fail: "3 usages of dangerouslySetInnerHTML with untrusted input: app/posts/[id]/page.tsx renders post.body directly".
  • Cross-reference: For full injection coverage (CSRF, SSRF, prototype pollution, XXE), run the api-security or security-hardening audit.
  • Remediation: Pipe the HTML through a sanitizer before rendering:
    import DOMPurify from 'isomorphic-dompurify'
    <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(post.body) }} />
    

Taxons

History