XSS in React — dangerouslySetInnerHTML and the specific bugs we see
React escapes interpolations by default, which eliminates 95% of XSS. The remaining 5% kills apps. Here are the exact patterns that slip through.
React's JSX is XSS-safe for variable interpolations. But `dangerouslySetInnerHTML`, certain `href` patterns, and user-controlled React children can re-introduce XSS. This guide covers the five patterns we see most in AI-built apps.
What it is
Cross-site scripting lets an attacker inject JavaScript that runs in other users' browsers. In React, the default text interpolation is safe. XSS requires opting out of the defaults.
Vulnerable example
// Pattern 1: dangerouslySetInnerHTML with untrusted content
<div dangerouslySetInnerHTML={{ __html: userBio }} />
// Pattern 2: javascript: URL in href
<a href={userLink}>{userLink}</a>
// user submits: javascript:alert(document.cookie)
// Pattern 3: React children from JSON.parse
<div>{JSON.parse(serverData).children}</div>Fixed example
// 1. Sanitize HTML input with DOMPurify
import DOMPurify from "isomorphic-dompurify";
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userBio) }} />
// 2. Validate the URL protocol
function safeHref(u: string) {
try { return /^https?:$/.test(new URL(u).protocol) ? u : "#"; }
catch { return "#"; }
}
<a href={safeHref(userLink)}>{userLink}</a>
// 3. Never render JSX from user-controlled structures
<div>{String(serverData).slice(0, 1000)}</div>How Securie catches it
apps/web/components/MarkdownView.tsx:28XSS in React
Securie's XSS specialist flags every dangerouslySetInnerHTML call with user-path input, every href/src binding to user data, and every SSR path that embeds attacker-controlled JSON without escaping.
// 1. Sanitize HTML input with DOMPurify
import DOMPurify from "isomorphic-dompurify";
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userBio) }} />
// 2. Validate the URL protocol
function safeHref(u: string) {
try { return /^https?:$/.test(new URL(u).protocol) ? u : "#"; }
catch { return "#"; }
}
<a href={safeHref(userLink)}>{userLink}</a>
// 3. Never render JSX from user-controlled structures
<div>{String(serverData).slice(0, 1000)}</div>Checklist
- dangerouslySetInnerHTML only used with DOMPurify-sanitized content
- URL props validated for http/https protocols
- User-controlled data never builds React element structures
- Strict Content-Security-Policy present (defense in depth)
- Server-rendered JSON escaped to avoid closing </script> injection
FAQ
Does React handle everything?
React handles text. It does not handle HTML strings, URL protocols, or user-controlled object structures. Those three are where XSS comes from in React apps.
Related guides
Row-Level-Security bypass is the most common data leak in vibe-coded apps. Here is exactly how it happens, how attackers find it, and how to fix it in Next.js + Supabase with one policy update.
BOLA is the top item on the OWASP API Security Top 10 for a reason — every AI coding assistant introduces it by default. Learn what it looks like in Next.js, how to exploit it, and how to fix it.
IDOR is the classic name for an authorization bug where a user can change an ID in a URL and access data they should not see. It is BOLA's older cousin and still ships in half of all new apps.
Every week founders tweet about their OpenAI bill going from $10 to $10,000 overnight. Usually the cause is an API key committed to a public repo. Here is why it happens in Next.js specifically and how to stop it in five minutes.