XSS in React Applications
React provides strong default XSS protection through JSX auto-escaping, but several common patterns bypass these safeguards. dangerouslySetInnerHTML, javascript: URLs in href attributes, and direct DOM manipulation through refs create exploitable XSS vectors that attackers actively target.
Scan Your React AppHow XSS Manifests in React
The most common XSS vector in React is dangerouslySetInnerHTML, which renders raw HTML strings without escaping. Developers use it for rich text editors, markdown rendering, and embedding third-party content. A second vector is href attribute injection. React does not prevent javascript: URLs in anchor tags, so <a href={userInput}> is exploitable if userInput contains javascript:alert(document.cookie). Direct DOM manipulation via refs (ref.current.innerHTML = userInput) bypasses React's virtual DOM entirely. Similarly, using document.write() or jQuery alongside React creates escape hatches that attackers exploit. Third-party component libraries that accept HTML strings (e.g., rich text editors, tooltip libraries) can also introduce XSS if their inputs are not sanitized.
Real-World Impact
A React-based SaaS dashboard allowed users to customize their profile bio with rich text. The bio field used dangerouslySetInnerHTML without sanitization. An attacker crafted a bio containing an invisible iframe that loaded a keylogger, capturing credentials of anyone who viewed the attacker's profile page. In another case, a React app using user-provided URLs in href attributes was exploited with javascript: payloads to perform actions on behalf of logged-in users, including changing email addresses and resetting passwords.
Step-by-Step Fix
Replace dangerouslySetInnerHTML with safe alternatives
Use a React markdown component or sanitization library instead of raw HTML injection.
// UNSAFE
<div dangerouslySetInnerHTML={{ __html: userContent }} />
// SAFE - using react-markdown
import ReactMarkdown from 'react-markdown';
<ReactMarkdown>{userContent}</ReactMarkdown>
// SAFE - using DOMPurify when HTML is required
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userContent);
<div dangerouslySetInnerHTML={{ __html: clean }} />Validate URLs in href attributes
Create a URL validation utility that blocks javascript: and data: protocols.
function sanitizeUrl(url: string): string {
try {
const parsed = new URL(url);
if (['http:', 'https:', 'mailto:'].includes(parsed.protocol)) {
return url;
}
return '#';
} catch {
// Relative URLs are safe
if (url.startsWith('/')) return url;
return '#';
}
}
// Usage
<a href={sanitizeUrl(userProvidedUrl)}>Link</a>Avoid direct DOM manipulation
Replace ref-based innerHTML with React state updates.
// UNSAFE
const divRef = useRef<HTMLDivElement>(null);
divRef.current!.innerHTML = userInput;
// SAFE
const [content, setContent] = useState('');
setContent(userInput); // React auto-escapes in JSX
return <div>{content}</div>;Add Content Security Policy
Configure your server to send CSP headers that prevent inline script execution.
// Express server serving React app
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';"
);
next();
});Prevention Best Practices
1. Avoid dangerouslySetInnerHTML entirely when possible. Use React components instead of raw HTML strings. 2. If you must render HTML, sanitize with DOMPurify before rendering. 3. Validate all URLs before putting them in href attributes. Only allow http: and https: protocols. 4. Never use ref.current.innerHTML with user input. Use React state and JSX instead. 5. Audit third-party components that accept HTML strings and ensure they sanitize input. 6. Implement Content Security Policy headers on your server.
How to Test
1. Search your codebase for dangerouslySetInnerHTML and verify each usage sanitizes its input. 2. Find all <a href={...}> where href comes from user input and test with javascript:alert(1). 3. Search for ref.current.innerHTML and document.write() calls. 4. Test all user input fields with payloads: <img src=x onerror=alert(1)>, <svg onload=alert(1)>. 5. Use Vibe App Scanner to automatically detect XSS patterns in your React application.
Frequently Asked Questions
Does React automatically prevent XSS?
React automatically escapes string values in JSX expressions, which prevents most XSS. However, dangerouslySetInnerHTML, href attributes with javascript: URLs, and direct DOM manipulation via refs all bypass this protection. React's auto-escaping only applies to text content within JSX curly braces.
Is dangerouslySetInnerHTML always unsafe?
It is safe when the HTML comes from a trusted source (like your own CMS with proper sanitization) AND you sanitize it with a library like DOMPurify before rendering. It is always unsafe when rendering user-submitted content without sanitization.
Can XSS happen in React Native?
React Native does not render HTML, so traditional XSS is not possible. However, if you use WebView components with user-controlled content, XSS can occur within the WebView context. Always sanitize content passed to WebView.
Related Security Resources
Is Your React App Vulnerable to XSS?
VAS automatically scans for xss vulnerabilities in React applications and provides step-by-step remediation guidance with code examples.
Scans from $5, results in minutes. Get actionable fixes tailored to your React stack.