ESC

Type to search the knowledge base.

dangerouslySetInnerHTML Risks

dangerouslySetInnerHTML bypasses React escaping: XSS risks, sanitization, and safer alternatives.

intermediate3 min read
  • react
  • dangerouslysetinnerhtml-risks

React escapes text content by default — {user.name} will not become a live <script>. dangerouslySetInnerHTML opts out and injects raw HTML. The name is the warning.

Docs: dangerouslySetInnerHTML, XSS — OWASP.

The API

function ArticleBody({ html }) {
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

Note the shape: prop value is { __html: string }, not a bare string.

What goes wrong

If html includes attacker-controlled markup:

<img src=x onerror="steal(document.cookie)">

you execute attacker script in your origin. Stored XSS in CMS fields, markdown renderers without sanitization, and “preview” panes are classic sources.

Safer paths

Need Prefer
User text {text} normal children
Markdown Trusted pipeline + sanitizer (e.g. DOMPurify) server-side
Syntax highlight Library that emits React elements
Rich CMS Sanitize on save and on render; CSP as defense in depth
import DOMPurify from 'dompurify';

function SafeHtml({ html }) {
  const clean = DOMPurify.sanitize(html, { USE_PROFILES: { html: true } });
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

Sanitize on the server when you can so clients do not become the only gate. Still set a strong Content-Security-Policy if available in your stack.

SSR and hydration

Server and client must produce the same HTML string. Sanitizer config differences between environments cause hydration mismatches or security gaps.

Interview out-loud

“dangerouslySetInnerHTML injects raw HTML and bypasses React’s escaping, so untrusted strings are XSS. I avoid it for user content, sanitize when HTML is required, prefer element-based renderers, and combine with CSP. The prop shape is {{ __html }}.”

Further reading

Edge cases worth rehearsing

Interviewers and production incidents cluster around the same edges: first render versus update, empty and loading states, Strict Mode double setup, concurrent interruptions, and what happens when identity (key, route params, user id) changes mid-edit. Walk one concrete user journey end-to-end — open, edit, navigate away, come back — and say which state survives.

Prefer fixing data flow and ownership before reaching for memoization or micro-optimizations. Prefer event handlers over effects when a user action is the trigger. Prefer deriving values during render over mirroring props into state. Prefer stable list keys from business ids. Measure with the profiler when performance is the claim.

When you cite an API, mention one failure mode: abort on unmount, serializable props across server/client boundaries, focus restoration for dialogs, or cache invalidation after a mutation. Specific beats generic every time.

Edge cases worth rehearsing

Interviewers and production incidents cluster around the same edges: first render versus update, empty and loading states, Strict Mode double setup, concurrent interruptions, and what happens when identity (key, route params, user id) changes mid-edit. Walk one concrete user journey end-to-end — open, edit, navigate away, come back — and say which state survives.

Prefer fixing data flow and ownership before reaching for memoization or micro-optimizations. Prefer event handlers over effects when a user action is the trigger. Prefer deriving values during render over mirroring props into state. Prefer stable list keys from business ids. Measure with the profiler when performance is the claim.

When you cite an API, mention one failure mode: abort on unmount, serializable props across server/client boundaries, focus restoration for dialogs, or cache invalidation after a mutation. Specific beats generic every time.

Related guides