← Debugging Guides

阅读中文版本

XSS via dangerouslySetInnerHTML — Unescaped HTML

Fix XSS from dangerouslySetInnerHTML by rendering text normally, sanitizing any required HTML, and relying on React escaping by default.

intermediatedebuggingsecurityxssreact

Symptoms

  • User content injected via dangerouslySetInnerHTML executes a script
  • A stored comment renders unsanitized HTML
  • A scanner reports a DOM XSS sink

Possible causes

  • User-controlled HTML is passed to dangerouslySetInnerHTML
  • A sanitizer is applied but with an overly permissive allowlist
  • Markdown or rich text is rendered after conversion without sanitizing the HTML

Diagnosis steps

Find dangerous HTML sinks

grep -rn "dangerouslySetInnerHTML\|innerHTML\|v-html" src/

Interpretation

Each sink that receives user content is a potential XSS vector.

Test with a benign payload

git grep -n "<img src=x onerror" || echo 'no literal payload committed'

Interpretation

Rendering a controlled payload in staging verifies whether scripts execute.

Root cause

dangerouslySetInnerHTML injects raw HTML, bypassing React escaping. When it receives user-controlled content, an attacker can include event handlers or script tags that execute in other users' sessions.

Fix

Render untrusted content as text, and when HTML is genuinely required, sanitize it with a vetted library and a strict allowlist of tags and attributes before injecting, or render in a sandboxed iframe.

Risk note

Hand-rolled sanitizers and regex filtering are easily bypassed; use a maintained library such as DOMPurify.

Verify the fix

Render the payload again and confirm no script executes and safe markup still displays; verify the sanitizer strips onerror and script tags.

Symptoms

This debugging guide addresses “XSS via dangerouslySetInnerHTML — Unescaped HTML”. Start from the symptoms, follow the ordered diagnostics, and confirm each hypothesis before moving on; the fix and verification steps then close the loop.

Observed symptoms include:

  • User content injected via dangerouslySetInnerHTML executes a script
  • A stored comment renders unsanitized HTML
  • A scanner reports a DOM XSS sink

Reproduce and Diagnose

Reproduce the failure in isolation before changing any code. Work through the checks below in order; each one narrows the search space.

Step 1: Find dangerous HTML sinks

grep -rn "dangerouslySetInnerHTML\|innerHTML\|v-html" src/

Each sink that receives user content is a potential XSS vector.

Step 2: Test with a benign payload

git grep -n "<img src=x onerror" || echo 'no literal payload committed'

Rendering a controlled payload in staging verifies whether scripts execute.

Likely contributors, in order of frequency:

  • User-controlled HTML is passed to dangerouslySetInnerHTML
  • A sanitizer is applied but with an overly permissive allowlist
  • Markdown or rich text is rendered after conversion without sanitizing the HTML

Root Cause

dangerouslySetInnerHTML injects raw HTML, bypassing React escaping. When it receives user-controlled content, an attacker can include event handlers or script tags that execute in other users’ sessions.

The Fix

Render untrusted content as text, and when HTML is genuinely required, sanitize it with a vetted library and a strict allowlist of tags and attributes before injecting, or render in a sandboxed iframe.

Watch out: Hand-rolled sanitizers and regex filtering are easily bypassed; use a maintained library such as DOMPurify.

import DOMPurify from 'dompurify';

// ❌ raw user HTML
// <div dangerouslySetInnerHTML={{ __html: userHtml }} />

// ✅ sanitize with a strict allowlist, then inject
const clean = DOMPurify.sanitize(userHtml, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
  ALLOWED_ATTR: ['href'],
});
// <div dangerouslySetInnerHTML={{ __html: clean }} />

Verify and Prevent

Render the payload again and confirm no script executes and safe markup still displays; verify the sanitizer strips onerror and script tags.

Once verified, add a regression test that reproduces the original symptom so the failure cannot silently return.

This class of failure is usually environmental rather than pure logic, so capture the exact command, the environment variables, and the working directory in your bug report. Reproduce it in the smallest possible script, then widen the scope only once the minimal case is understood. A short written checklist for this failure mode saves hours the next time it appears.