React has a good security default: when you render a value in JSX, it escapes it, so a string containing HTML shows up as text instead of running. That default quietly prevents most cross-site scripting. Which means when XSS does appear in a React app, it usually comes through the one prop that turns the default off: dangerouslySetInnerHTML. The name is a warning, and it is accurate.
Why AI tools reach for it
Sometimes you really do need to render HTML rather than text: rich content from a markdown editor, a description field, something from a CMS. When an assistant hits that need, dangerouslySetInnerHTML is the direct way to do it, and it works immediately. The step it skips is sanitizing the HTML first, because the feature renders correctly without it. On your own clean content nothing looks wrong; the hole only opens when the HTML includes something a user supplied.
// the risky pattern: user content rendered as raw HTML
<div dangerouslySetInnerHTML={{ __html: comment.body }} />
// if comment.body contains:
// <img src=x onerror="/* attacker script */">
// the script runs in every viewer's browserFrom there it is a normal XSS: the attacker's script runs in your users' browsers with their session, so it can act as them and read what the page can read. The impact is worse if a token is kept somewhere a script can reach, which is one reason to keep auth tokens out of localStorage. The concept in full is in what XSS is.
Want to see exactly what Prbl flags? Watch it scan a demo app, no repo or account needed.
See a live scan →How to use it safely
You do not have to avoid the prop; you have to sanitize what you pass to it. Run the HTML through a maintained sanitizer like DOMPurify first, which parses it and removes anything that can execute while keeping safe formatting.
import DOMPurify from "dompurify";
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(comment.body),
}}
/>Two rules make this reliable. First, prefer plain text rendering whenever you can, since React's default already protects it and you avoid the escape hatch entirely. Second, when you do need HTML, always sanitize with a library, never a hand-written regex, because HTML has too many edge cases for a filter to catch. The full walkthrough is in the fix for dangerouslySetInnerHTML XSS, and a Content Security Policy adds a second layer that limits what injected script can do.
Frequently asked questions
Isn't React supposed to be safe from XSS?
React is safe by default, which is exactly why the exceptions matter. When you render a value in JSX with curly braces, React escapes it, so a string of HTML shows up as text rather than running. That default handles the vast majority of cases. XSS creeps back in through the deliberate escape hatch, dangerouslySetInnerHTML, which tells React to insert raw HTML and skip the escaping. So the risk is not React; it is the one place you told it to stop protecting you.
When do I actually need dangerouslySetInnerHTML?
When you genuinely need to render HTML rather than text, for example showing rich content from a markdown editor or a CMS. That is a real need, and the prop exists for it. The rule is that whatever HTML you pass must be sanitized first if any of it could have come from a user. If the content is fully your own and never touched by user input, the risk is lower, but sanitizing is cheap insurance.
How do I sanitize the HTML safely?
Run it through a maintained sanitizer such as DOMPurify before passing it to dangerouslySetInnerHTML. DOMPurify parses the HTML and strips anything that can execute, like script tags and inline event handlers, while keeping safe formatting. Do not try to write your own sanitizer with a regex; HTML has too many edge cases and hand-rolled filters miss them. Use the library, keep it updated.
What can an attacker actually do with a React XSS bug?
Run their script in your users' browsers with the users' session. That means reading anything the page can read, acting as the user, and stealing session data, especially if a token is kept somewhere scripts can reach, like localStorage. It turns one injected comment or profile field into account takeover for anyone who views it. That is why a single unsanitized dangerouslySetInnerHTML on user content is a serious hole.
Find every escape hatch in your app
The safe and unsafe versions look almost identical, one just has a sanitize call, so it is easy to miss in review. A scan flags every dangerouslySetInnerHTML that renders unsanitized content, with the file and line, so you can wrap the risky ones before an attacker finds them. Run a free scan and see where your app inserts raw HTML.