XSS in React 2025: Modern Attacks and Defenses
React's automatic escaping makes XSS harder—but not impossible. Signal had to patch a React-based XSS vulnerability related to improper HTML handling. A 2024 security analysis revealed that XSS vulnerabilities continue to affect React applications, often with more severe consequences than traditional apps.
This guide covers the specific ways XSS bypasses React's protections and how to defend against them in 2025.
How React Protects Against XSS
React automatically escapes values embedded in JSX:
// Safe - React escapes the script tags
const userInput = "<script>alert('xss')</script>";
return <div>{userInput}</div>;
// Renders: <script>alert('xss')</script>This default protection stops most XSS attacks. But several APIs and patterns bypass it entirely.
Attack Vector 1: dangerouslySetInnerHTML
The most common React XSS vulnerability:
// VULNERABLE - Raw HTML injection
function BlogPost({ content }) {
return <div dangerouslySetInnerHTML={{ __html: content }} />;
}
// Attack payload in content:
// "<img src=x onerror=alert(document.cookie)>"Defense: DOMPurify
Always sanitize before using dangerouslySetInnerHTML:
import DOMPurify from 'dompurify';
function SafeBlogPost({ content }) {
const sanitizedContent = DOMPurify.sanitize(content, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'a'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
});
return <div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />;
}Better: Create a SafeHTML Component
Encapsulate sanitization in a reusable component:
import DOMPurify from 'dompurify';
interface SafeHTMLProps {
html: string;
allowedTags?: string[];
className?: string;
}
export function SafeHTML({ html, allowedTags, className }: SafeHTMLProps) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: allowedTags || ['p', 'br', 'strong', 'em', 'a'],
ALLOWED_ATTR: ['href', 'class'],
ADD_ATTR: ['target'],
});
return (
<div
className={className}
dangerouslySetInnerHTML={{ __html: clean }}
/>
);
}
// Usage
<SafeHTML html={userContent} allowedTags={['p', 'br']} />Attack Vector 2: URL Injection (href/src)
React doesn't sanitize URL attributes:
// VULNERABLE - javascript: URLs execute code
function UserLink({ url, label }) {
return <a href={url}>{label}</a>;
}
// Attack: url = "javascript:alert(document.cookie)"Defense: URL Validation
function isValidURL(url: string): boolean {
try {
const parsed = new URL(url);
// Only allow safe protocols
return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
} catch {
return false;
}
}
function SafeLink({ url, label }: { url: string; label: string }) {
if (!isValidURL(url)) {
return <span>{label}</span>; // Render as plain text
}
return (
<a href={url} rel="noopener noreferrer">
{label}
</a>
);
}Attack Vector 3: ref-based DOM Manipulation
Direct DOM access bypasses React's protection:
// VULNERABLE - Direct innerHTML assignment
function DangerousComponent({ userContent }) {
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (divRef.current) {
divRef.current.innerHTML = userContent; // XSS!
}
}, [userContent]);
return <div ref={divRef} />;
}Defense: Avoid Direct DOM Manipulation
// SAFE - Let React handle rendering
function SafeComponent({ userContent }) {
const sanitized = DOMPurify.sanitize(userContent);
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}Attack Vector 4: Server-Side Rendering (SSR)
SSR can expose XSS if state isn't properly escaped:
// VULNERABLE - Server-rendered state injection
function ServerPage({ initialData }) {
return (
<html>
<body>
<div id="root" />
<script
dangerouslySetInnerHTML={{
__html: `window.__INITIAL_STATE__ = ${JSON.stringify(initialData)}`
}}
/>
</body>
</html>
);
}
// Attack: initialData contains </script><script>alert(1)</script>Defense: Serialize-JavaScript
import serialize from 'serialize-javascript';
function SafeServerPage({ initialData }) {
return (
<html>
<body>
<div id="root" />
<script
dangerouslySetInnerHTML={{
__html: `window.__INITIAL_STATE__ = ${serialize(initialData, { isJSON: true })}`
}}
/>
</body>
</html>
);
}Attack Vector 5: Third-Party Components
External libraries may not follow security best practices:
// POTENTIALLY VULNERABLE
// Some rich text editors render HTML without sanitization
import { RichTextEditor } from 'some-library';
function Editor({ content }) {
return <RichTextEditor value={content} />;
}Defense: Audit and Wrap
// Wrap third-party components with sanitization
function SafeRichTextDisplay({ content }) {
const sanitized = DOMPurify.sanitize(content);
return <RichTextEditor value={sanitized} readOnly />;
}Defense Layer: Content Security Policy
CSP provides defense-in-depth:
// next.config.js (Next.js example)
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: `
default-src 'self';
script-src 'self' 'nonce-{RANDOM}';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
`.replace(/\n/g, '')
}
];Defense Layer: ESLint Rules
Detect dangerous patterns automatically:
{
"plugins": ["react", "jam3"],
"rules": {
"react/no-danger": "warn",
"jam3/no-sanitizer-with-danger": "error",
"react/jsx-no-script-url": "error",
"react/jsx-no-target-blank": "error"
}
}React XSS Prevention Checklist
dangerouslySetInnerHTML
- [ ] Always sanitize with DOMPurify before use
- [ ] Create wrapper component for consistent sanitization
- [ ] Configure allowed tags/attributes appropriately
- [ ] Use ESLint rules to flag unsanitized usage
URLs
- [ ] Validate all user-provided URLs
- [ ] Block javascript: and data: protocols
- [ ] Use allowlist of permitted protocols
- [ ] Add rel="noopener noreferrer" to external links
SSR
- [ ] Use serialize-javascript for state serialization
- [ ] Escape HTML entities in server-rendered content
- [ ] Implement CSP headers
General
- [ ] Enable strict CSP
- [ ] Audit third-party components
- [ ] Keep dependencies updated
- [ ] Run security scanning in CI/CD
Practice XSS Attacks
Understanding how XSS works is essential for building secure React applications. Practice identifying and exploiting XSS vulnerabilities in our XSS challenges.
---
React security evolves with each release. This guide will be updated as new attack vectors and defenses emerge. Last updated: December 2025.
Stay ahead of vulnerabilities
Weekly security insights, new challenges, and practical tips. No spam.
Unsubscribe anytime. No spam, ever.