React's primary XSS threats arise from bypassing its built-in escaping mechanisms via dangerouslySetInnerHTML, handling untrusted URLs, and server-side rendering; prevention relies on sanitization, strict Content Security Policies (CSP), and using TypeScript to enforce a trust boundary.
React is designed with XSS prevention in mind, primarily through automatic escaping of values embedded in JSX. This mechanism ensures that by default, any data rendered is treated as plain text, not executable code. However, this protection is not absolute. XSS vulnerabilities are reintroduced when developers intentionally or accidentally bypass these safe defaults. The major threats and their corresponding prevention strategies are detailed below.
dangerouslySetInnerHTML: This is the most significant threat vector. It is a direct replacement for innerHTML and instructs React to bypass its automatic escaping, rendering the raw HTML string directly into the DOM. If this string contains user-generated or untrusted content, it creates a direct path for XSS attacks.
Unsafe URL Schemes: React applications can be vulnerable when they construct anchor (<a>) tags with user-controlled href attributes. An attacker could provide a URL with a javascript: scheme (e.g., javascript:alert('XSS')), which, when clicked, would execute arbitrary code in the context of the page.
Third-Party Libraries and Custom Renderers: Using libraries that manipulate the DOM outside of React's control, or creating custom components that use native DOM APIs like innerHTML or eval, bypasses React's security model. This is a common source of XSS in rich-text editors or Markdown renderers.
Server-Side Rendering (SSR) Hydration: While less common, if JSON data is embedded in the initial HTML for SSR without proper escaping, or if there is a mismatch between server-rendered and client-rendered content due to unsafe string concatenation, it can create DOM-based XSS opportunities.
A layered defense strategy is the most effective way to secure a React application against XSS. These strategies range from secure coding practices to implementing robust security headers and using the type system to enforce safety.
The Primary Solution: Never pass untrusted content directly to dangerouslySetInnerHTML. The standard practice is to use a dedicated sanitization library like DOMPurify.
How to Implement: Before rendering, pass the raw HTML string through DOMPurify's sanitize method. This strips out any potentially malicious code (like <script> tags or onerror event handlers), leaving only safe HTML.
Code Example:
import DOMPurify from 'dompurify';
function SafeHTML({ htmlContent }) {
const cleanHtml = DOMPurify.sanitize(htmlContent);
return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;
}
Alternative: For scenarios where you need to render HTML as React components, consider pairing DOMPurify with html-react-parser. This combination sanitizes the HTML and then parses it into actual React elements, offering more control than dangerouslySetInnerHTML.
The Primary Solution: Treat user-provided URLs as untrusted and validate them against an allowlist of safe schemes (e.g., https:, http:, mailto:).
How to Implement: Create a helper function that checks the scheme of any URL before it is placed in an href, src, or other navigation-affecting attribute. Reject or sanitize any URL that does not start with a safe protocol.
Code Example:
function safeUrl(url) {
try {
const parsedUrl = new URL(url);
const allowedSchemes = ['http:', 'https:', 'mailto:'];
if (!allowedSchemes.includes(parsedUrl.protocol)) {
return '#'; // Return a safe default
}
return url;
} catch (e) {
return '#'; // Handle invalid URL formats
}
}
function ExternalLink({ userProvidedUrl, children }) {
return <a href={safeUrl(userProvidedUrl)}>{children}</a>;
}
Related Tooling: The eslint-plugin-react includes a rule (react/jsx-no-target-blank) that, while focused on rel="noreferrer", helps mitigate a related risk when opening external links in a new tab.
The Primary Solution: CSP is a powerful HTTP header that acts as a browser-side firewall, telling the browser to only load resources (scripts, styles, images) from explicitly trusted sources.
How to Implement: Configure your web server to send the Content-Security-Policy header. A strict policy can completely block the execution of inline scripts, which is a primary vector for XSS.
Example Header: Content-Security-Policy: default-src 'self'; script-src 'self' This policy only allows scripts from your own domain and blocks all inline scripts (unsafe-inline is not allowed), making many XSS attacks impossible even if an attacker finds an injection point.
Concept: Define types like UntrustedString and TrustedHtml. Functions that accept user input return UntrustedString. Only a dedicated sanitizer can convert an UntrustedString into a TrustedHtml. Your components are then designed to only accept TrustedHtml, making it impossible to accidentally skip sanitization.
Why It Works: This moves the responsibility of security from runtime discipline (which is error-prone) to compile-time verification (which is enforced by the tooling).
Avoid eval() and innerHTML: These are extremely dangerous in any context and should never be used on any string that may have originated from a user.
Keep Dependencies Updated: Regularly update React and all third-party libraries. Vulnerabilities are frequently discovered and patched in libraries, including in core React itself.
Set HttpOnly Cookies: For sensitive data like authentication tokens stored in cookies, always set the HttpOnly flag. This prevents them from being accessed by any client-side JavaScript, rendering them useless in the event of an XSS exploit.