How do I fix XSS in React?

Direct answer

React escapes values in JSX automatically, so most XSS in React apps comes from the four places that bypass it: `dangerouslySetInnerHTML`, user-controlled URLs in `href` or `src`, direct DOM access through refs, and user data injected into server-rendered HTML. Fix the first by sanitising with DOMPurify, and the second by validating the protocol.

Muhammad HasanUpdated

React escapes anything interpolated into JSX, which removes the most common form of this bug by default. <div>{userInput}</div> renders a script tag as text. That's why XSS in React apps is comparatively rare and why, when it happens, it's in one of a small number of places.

dangerouslySetInnerHTML

The name is a warning and it gets used anyway, usually to render rich text from a CMS or user content.

// Vulnerable
<div dangerouslySetInnerHTML={{ __html: post.body }} />

If post.body can contain anything a user wrote, that executes. Sanitise it:

import DOMPurify from 'dompurify';

<div dangerouslySetInnerHTML={{
  __html: DOMPurify.sanitize(post.body)
}} />

Sanitise at render rather than on save. Sanitising once on input means any change to your sanitiser rules doesn't apply to data already stored, and you have no way of knowing what got through under the old rules.

User-controlled URLs

React escapes text, not protocols.

// Vulnerable
<a href={user.website}>Website</a>

A value of javascript:alert(document.cookie) executes on click. Same applies to src on iframes and to anything accepting a URL.

function safeUrl(url) {
  try {
    const parsed = new URL(url, window.location.origin);
    return ['http:', 'https:'].includes(parsed.protocol) ? url : '#';
  } catch {
    return '#';
  }
}

Refs and direct DOM access

Anything reaching past React into the DOM leaves React's escaping behind.

// Vulnerable
useEffect(() => {
  ref.current.innerHTML = content;
}, [content]);

Use textContent for text, or sanitise if the markup is genuinely needed.

Third-party libraries that manipulate the DOM directly, chart tooltips, rich text editors, older jQuery plugins wrapped in components. Deserve the same suspicion.

Server-rendered state

If you serialise state into the HTML document, you've left React's protection entirely.

// Vulnerable
<script dangerouslySetInnerHTML={{
  __html: `window.__DATA__ = ${JSON.stringify(data)}`
}} />

JSON.stringify doesn't escape </script>, so user data containing that string closes the tag early and everything after it is markup. Escape the output or use a library built for the job.

Defence in depth

A Content Security Policy limits what an XSS payload can do even if one lands. It isn't a substitute for the fixes above, but it's the difference between a bug and an incident.

Auditing what you already have

Scanners handle dangerouslySetInnerHTML reasonably well because it's a distinctive call with a traceable input. They do less well on the URL case, where the vulnerable code is an ordinary attribute assignment, and worse on third-party DOM manipulation.

Grep for dangerouslySetInnerHTML, innerHTML and .href = as a first pass. It's crude and it will find most of what's there.

Go deeper

Fast remediation with Kolega

Related answers

See what your own repository returns

Connect a repo and run a scan. No credit card, no pipeline changes.

Get started for free