Why You Still Need to Worry About XSS in React
React has a reputation for being safe against Cross-Site Scripting (XSS) attacks. And to a degree, that is true. React automatically escapes values embedded in JSX before rendering them to the DOM, which neutralizes many common attack vectors.
But here is the catch: React is not fully XSS-proof. Developers regularly introduce vulnerabilities through unsafe patterns like dangerouslySetInnerHTML, unvalidated URL schemes, and improperly handled third-party data. A single oversight can expose your users to stolen sessions, data theft, and worse.
In this guide, we walk through the most common XSS vulnerabilities in React apps and show you exactly how to prevent them with real, production-ready code examples.
What Is Cross-Site Scripting (XSS)?
Cross-Site Scripting (XSS) is a type of security vulnerability where an attacker injects malicious scripts into a web page viewed by other users. The browser executes the script because it trusts the content served by the domain.
There are three main types of XSS attacks:
| Type | Description | Example Trigger |
|---|---|---|
| Stored XSS | Malicious script is stored on the server (e.g., in a database) and served to users. | A comment field that saves and renders unescaped HTML. |
| Reflected XSS | Script is reflected off a web server via a URL parameter or form submission. | A search page that displays the raw query string in results. |
| DOM-based XSS | The vulnerability exists in client-side code that manipulates the DOM unsafely. | Using document.innerHTML with user-supplied data. |
React apps are most susceptible to DOM-based XSS and stored XSS when developers bypass the framework’s built-in protections.

How React Protects Against XSS by Default
Before diving into vulnerabilities, it is important to understand what React does right out of the box.
When you write JSX like this:
const userInput = '<script>alert("hacked")</script>';
return <div>{userInput}</div>;
React automatically escapes the string before inserting it into the DOM. The browser will display the literal text <script>alert("hacked")</script> instead of executing it. This is because React uses createElement() rather than innerHTML under the hood.
This default escaping covers most simple cases. However, several common development patterns completely bypass this protection.

7 Proven Techniques to Prevent XSS in React
1. Avoid dangerouslySetInnerHTML (or Use It Safely)
The most notorious source of XSS in React is dangerouslySetInnerHTML. This prop tells React to inject raw HTML directly into the DOM, completely bypassing its automatic escaping.
The Vulnerable Pattern
// DANGEROUS: Never do this with unsanitized user data
function Comment({ htmlContent }) {
return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
}
If htmlContent comes from user input or an external API, an attacker can inject any script they want.
The Safe Alternative
If you absolutely must render HTML (for example, from a rich text editor), always sanitize it first using a library like DOMPurify:
import DOMPurify from 'dompurify';
function Comment({ htmlContent }) {
const sanitized = DOMPurify.sanitize(htmlContent);
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}
DOMPurify strips out all dangerous elements like <script>, event handlers (onerror, onclick), and malicious attributes while preserving safe HTML formatting.
Rule of thumb: If you can avoid dangerouslySetInnerHTML entirely, do so. If you cannot, DOMPurify is non-negotiable.
2. Sanitize All User Inputs on Both Client and Server
Never trust user input. Even if React escapes output in JSX, unsanitized data can cause problems in other contexts (URLs, API calls, server-side rendering).
Client-Side Sanitization Example
import DOMPurify from 'dompurify';
function handleSubmit(formData) {
const cleanName = DOMPurify.sanitize(formData.name);
const cleanBio = DOMPurify.sanitize(formData.bio, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'],
ALLOWED_ATTR: []
});
// Now safe to store or display
saveToDatabase({ name: cleanName, bio: cleanBio });
}
Server-Side Validation (Node.js/Express Example)
const expressSanitizer = require('express-sanitizer');
const { body, validationResult } = require('express-validator');
app.use(expressSanitizer());
app.post('/api/comments',
body('content').trim().escape(),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process sanitized input
}
);
Always apply sanitization on the server side too. Client-side checks can be bypassed by attackers who send requests directly to your API.
3. Implement Content Security Policy (CSP) Headers
A Content Security Policy is one of the most powerful defenses against XSS. CSP tells the browser which sources of content are allowed to execute, effectively blocking injected scripts even if they make it into your HTML.
Setting CSP Headers in Your Server
For a Node.js/Express backend serving a React app:
const helmet = require('helmet');
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.yourdomain.com"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
},
})
);
CSP via Meta Tag (if you cannot set headers)
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" />
Key CSP directives for preventing XSS in React apps:
- script-src ‘self’ – Only allow scripts from your own domain. This blocks inline scripts injected by attackers.
- object-src ‘none’ – Blocks plugins like Flash that can be exploited.
- base-uri ‘self’ – Prevents attackers from changing the base URL of your page.
Important note: If you use nonces or hashes with CSP (recommended for inline scripts in production), make sure your build pipeline generates them correctly. Libraries like helmet make this process straightforward.
4. Validate and Sanitize URLs to Block javascript: Protocols
A lesser-known XSS vector in React is through href and src attributes. React does not automatically block dangerous URL schemes.
The Vulnerable Pattern
// DANGEROUS: User-controlled href
function UserProfile({ website }) {
return <a href={website}>Visit Website</a>;
}
An attacker could set website to javascript:alert(document.cookie), and the browser would execute the script when clicked.
The Safe Pattern
function UserProfile({ website }) {
const isValidUrl = (url) => {
try {
const parsed = new URL(url);
return ['https:', 'http:'].includes(parsed.protocol);
} catch {
return false;
}
};
const safeUrl = isValidUrl(website) ? website : '#';
return <a href={safeUrl} rel="noopener noreferrer">Visit Website</a>;
}
Always validate that user-supplied URLs use https: or http: protocols before rendering them in anchor tags, image sources, or iframes.
5. Escape HTML Entities When Rendering Dynamic Data Outside JSX
If you ever need to insert dynamic content into the DOM outside of React’s rendering flow (for example, using document.title or interacting with third-party libraries), you need to escape HTML entities manually.
function escapeHtml(str) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return str.replace(/[&<>"']/g, (char) => map[char]);
}
// Usage
document.title = escapeHtml(userProvidedTitle);
Better yet, use a well-tested library like he (HTML entities) for encoding:
import he from 'he';
document.title = he.encode(userProvidedTitle);
6. Keep Dependencies Updated and Audit Regularly
Many XSS vulnerabilities enter React applications through outdated third-party packages. A compromised or vulnerable npm package can inject malicious code into your entire application.
Make dependency auditing part of your development workflow:
- Run regular audits: Use
npm auditoryarn auditto check for known vulnerabilities. - Automate updates: Use tools like Dependabot or Renovate to get automatic pull requests for security patches.
- Lock your versions: Use
package-lock.jsonoryarn.lockto ensure consistent installs across environments. - Review new packages carefully: Before adding a new dependency, check its maintenance status, download count, and known issues.
# Run this regularly in your CI/CD pipeline
npm audit --production
# Fix vulnerabilities automatically where possible
npm audit fix
7. Use TypeScript and Linting Rules to Catch Unsafe Patterns
Catching XSS vulnerabilities before they reach production is far better than fixing them after an attack. TypeScript and ESLint can help you enforce safe coding patterns across your team.
ESLint Rules for XSS Prevention
Install the eslint-plugin-react package and enable these rules in your ESLint config:
{
"rules": {
"react/no-danger": "error",
"react/no-danger-with-children": "error",
"no-eval": "error",
"no-implied-eval": "error"
}
}
The react/no-danger rule will flag every use of dangerouslySetInnerHTML in your codebase, forcing developers to either justify its use or find a safer approach.
For teams that need dangerouslySetInnerHTML in specific cases, consider setting the rule to "warn" and requiring code review approval for any exceptions.

Quick Reference: XSS Prevention Checklist for React
| Action | Priority | Tools / Methods |
|---|---|---|
Avoid dangerouslySetInnerHTML |
Critical | ESLint rules, code review |
| Sanitize HTML before rendering | Critical | DOMPurify |
| Implement CSP headers | High | Helmet.js, server config |
| Validate URLs (block javascript: scheme) | High | URL constructor, allowlists |
| Server-side input validation | High | express-validator, Joi |
| Escape HTML entities outside JSX | Medium | he library, custom escape functions |
| Audit dependencies regularly | Medium | npm audit, Dependabot, Snyk |
| Enforce safe patterns with linting | Medium | ESLint + eslint-plugin-react |

Bonus: Testing Your React App for XSS Vulnerabilities
Prevention is only half the battle. You should also actively test your application for XSS issues:
- Manual testing: Try injecting common XSS payloads like
<img src=x onerror=alert(1)>into every input field and URL parameter in your app. - Automated scanning: Use tools like OWASP ZAP or Burp Suite to run automated XSS scans against your staging environment.
- Unit tests: Write tests that verify your sanitization functions strip malicious content correctly.
// Example Jest test for sanitization
import DOMPurify from 'dompurify';
test('strips script tags from user input', () => {
const malicious = '<p>Hello</p><script>alert("xss")</script>';
const clean = DOMPurify.sanitize(malicious);
expect(clean).toBe('<p>Hello</p>');
expect(clean).not.toContain('script');
});
test('blocks javascript: protocol in URLs', () => {
const isValidUrl = (url) => {
try {
const parsed = new URL(url);
return ['https:', 'http:'].includes(parsed.protocol);
} catch {
return false;
}
};
expect(isValidUrl('javascript:alert(1)')).toBe(false);
expect(isValidUrl('https://example.com')).toBe(true);
});
Frequently Asked Questions
Does React prevent XSS by default?
React escapes dynamic values rendered in JSX by default, which prevents many common XSS attacks. However, React does not protect against all XSS vectors. Features like dangerouslySetInnerHTML, user-controlled href attributes, and direct DOM manipulation can still introduce vulnerabilities. You need additional defenses like input sanitization and Content Security Policy headers for complete protection.
Is dangerouslySetInnerHTML always unsafe?
Not necessarily. dangerouslySetInnerHTML is safe when used with properly sanitized content. The key is to always run the HTML through a sanitization library like DOMPurify before passing it to dangerouslySetInnerHTML. The danger comes from using it with raw, unsanitized user input or untrusted API data.
What is the best library to sanitize HTML in React?
DOMPurify is the most widely recommended and battle-tested library for HTML sanitization in JavaScript applications. It is actively maintained, has a strong security track record, and allows you to configure exactly which tags and attributes are permitted.
How do Content Security Policy headers help prevent XSS?
CSP headers tell the browser which sources are allowed to load and execute scripts, styles, images, and other resources. Even if an attacker successfully injects a <script> tag into your page, a strict CSP policy (like script-src 'self') will prevent the browser from executing the inline script. CSP acts as a safety net that adds a critical second layer of defense.
Can XSS happen through React component props?
Yes. If you pass unsanitized data through props and that data ends up in dangerouslySetInnerHTML, a URL attribute, or a direct DOM manipulation call, it can lead to XSS. Always treat data from external sources (APIs, URL parameters, user input) as untrusted, regardless of how it flows through your component tree.
How often should I audit my React app for XSS vulnerabilities?
Security auditing should be continuous. Run npm audit in every CI/CD build, schedule periodic penetration tests (at least quarterly), and perform code reviews with a security focus whenever new features are added. Automated tools like Snyk or OWASP ZAP can run on every pull request to catch issues early.
Wrapping Up
React gives you a solid foundation for XSS protection, but it is not a complete solution on its own. To truly prevent XSS in React applications, you need a multi-layered approach: sanitize all user inputs, implement strict Content Security Policy headers, validate URLs, avoid unsafe APIs like dangerouslySetInnerHTML when possible, and test your app regularly.
The techniques covered in this guide will protect your React application against the vast majority of XSS attack vectors. Start with the critical items in the checklist above and work your way down. Every layer of defense you add makes your application significantly harder to exploit.
At Box Software, we build secure web applications from the ground up. If you need help auditing your React application for security vulnerabilities or implementing robust XSS prevention strategies, get in touch with our team.
