Mostly yes for plain text. React renders values you place in JSX as text, so a string does not turn into markup by accident. It does not protect escape hatches such as dangerouslySetInnerHTML, unchecked URL values, or HTML you sanitize badly. Those gaps cause most React XSS bugs.
What "safe by default" means
Cross-site scripting (XSS) happens when attacker-controlled data reaches the page as code instead of text. Modern frameworks reduce the odds because they steer developers toward templating and auto-escaping. OWASP says this plainly, then adds the catch: problems occur when a framework is used insecurely, and it names React's dangerouslySetInnerHTML without sanitizing the HTML as a typical example.
So the honest answer is that React removes the easy mistakes and leaves the deliberate ones to you.
Where the default protection ends
| Surface | Protected by default? | What you must do |
|---|---|---|
| Text in JSX | Yes, rendered as text | Nothing special |
| dangerouslySetInnerHTML | No, by design | Sanitize with a maintained sanitizer, or avoid it |
| href and src values from users | Partly, see the next section | Validate the URL scheme and destination yourself |
| Direct DOM access through refs | No | Treat it like plain JavaScript and avoid innerHTML |
| Third party components | Depends on the component | Review how they render HTML |
The React reference describes the dangerouslySetInnerHTML prop as something to use with extreme caution, and says that if the HTML is based on user data you risk introducing an XSS vulnerability. The long name is a deliberate warning label.
What about javascript: URLs?
React 16.9 started logging a warning when a javascript: URL appeared in an href, and the team said a future major release would throw an error. OWASP's cheat sheet now states that React 19 blocks javascript: URLs in src and href, but that this does not replace application-specific URL validation. A blocked scheme does not tell you the link goes somewhere safe.
A short review routine
- Search the codebase for dangerouslySetInnerHTML and list every hit.
- For each hit, trace where the HTML string comes from. If a user, a CMS field, an API response or a markdown renderer can influence it, it needs sanitizing.
- Search for places that set href or src from data, and check the scheme and host.
- Search for innerHTML and insertAdjacentHTML in code that uses refs.
- Add a lint rule so new uses get a human look. The eslint-plugin-react project ships a no-danger rule that flags the dangerous props, and its own docs say you can turn it off where you are certain the content is sanitized HTML.
Try it yourself
Click through it at your own pace, or open it full screen. Look at how a finding points to a specific line, which is how you would triage each dangerouslySetInnerHTML hit.
Safeguard scans repositories for vulnerable dependencies and uses reachability analysis to show which are called; you can try a free scan at gold.safeguard.sh/scan.
Quick check
Does React escape everything I render?
It escapes text rendered through JSX. It does not sanitize HTML passed to dangerouslySetInnerHTML, and it does not vet where a link points.
Is dangerouslySetInnerHTML ever fine?
Yes, when the HTML is trusted or has been sanitized by a maintained library and is not changed afterwards. Treat each use as a decision that needs a reviewer.
Sources
- React reference: common components
- React v16.9.0 release notes, deprecating javascript: URLs
- OWASP Cross Site Scripting Prevention Cheat Sheet
- eslint-plugin-react no-danger rule
Last reviewed: 19 October 2026.