React has built-in protections against some common web vulnerabilities, but those protections have limits. If your mental model is "React escapes everything so I'm safe," you are going to get burned. This guide covers what React protects, what it does not, and how to fill the gaps.
What React Gets Right
Automatic JSX Escaping
React's JSX automatically escapes values embedded in the DOM:
function UserGreeting({ name }) {
return <h1>Hello, {name}</h1>;
}
// If name is "<script>alert('xss')</script>", React renders it as text, not HTML
This prevents most reflected XSS attacks. The value is treated as text content, not HTML. React uses textContent under the hood, not innerHTML.
Virtual DOM Abstraction
The virtual DOM prevents many direct DOM manipulation attacks. Components express what the UI should look like, and React handles the DOM updates. This reduces the surface area for DOM-based XSS.
Where React Falls Short
dangerouslySetInnerHTML
The name says it all. This prop bypasses React's escaping:
// DANGEROUS: Never use with user-controlled content
function RichContent({ html }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
If you must render user-provided HTML, sanitize it first:
import DOMPurify from 'dompurify';
function RichContent({ html }) {
const sanitized = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href'],
});
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}
Use a restrictive allowlist, not a blocklist. DOMPurify's defaults are good, but tightening them further is better.
URL-Based XSS
React does not sanitize URLs in href and src attributes:
// Vulnerable to javascript: protocol XSS
function UserLink({ url, label }) {
return <a href={url}>{label}</a>;
}
// If url is "javascript:alert('xss')", the attack succeeds
Validate URLs before rendering:
function sanitizeURL(url) {
try {
const parsed = new URL(url);
if (!['http:', 'https:', 'mailto:'].includes(parsed.protocol)) {
return '#';
}
return url;
} catch {
return '#';
}
}
function UserLink({ url, label }) {
return <a href={sanitizeURL(url)}>{label}</a>;
}
Server-Side Rendering and Hydration
SSR introduces additional XSS vectors. Data serialized into the HTML page for hydration can be exploited:
// Vulnerable: user data in script tag
<script>
window.__INITIAL_STATE__ = {JSON.stringify(state)};
</script>
Use a serialization library that escapes dangerous characters:
import serialize from 'serialize-javascript';
<script>
window.__INITIAL_STATE__ = {serialize(state, { isJSON: true })};
</script>
serialize-javascript escapes </script> tags, HTML entities, and other injection vectors.
Content Security Policy
Set a strict CSP to limit what your React app can do:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.yourapp.com;
frame-ancestors 'none';
If you use Create React App or a custom webpack build, avoid 'unsafe-eval' in your script-src. Some older React development tools required it, but production builds do not.
For apps using styled-components or emotion, you will need a nonce-based CSP for inline styles instead of 'unsafe-inline'.
State Management Security
Sensitive Data in State
React state (including Redux stores) is accessible through React DevTools in development and can be inspected in memory in production. Never store secrets, tokens, or sensitive PII in component state longer than necessary.
// Bad: storing auth token in Redux
const authSlice = createSlice({
name: 'auth',
initialState: { token: null },
reducers: {
setToken: (state, action) => { state.token = action.payload; }
}
});
// Better: use httpOnly cookies for auth tokens
// The token never touches JavaScript at all
URL State and History
Data in the URL is visible in browser history, server logs, and the Referer header. Never put sensitive information in URL parameters or hash fragments.
Third-Party Component Risks
React's ecosystem relies heavily on third-party components. Each npm package you install is code that runs in your users' browsers with full access to their cookies, localStorage, and DOM.
Before adding a React dependency:
- Check the package's download count and maintenance status.
- Inspect its dependency tree. A UI component should not pull in 50 sub-dependencies.
- Check if it uses
dangerouslySetInnerHTMLinternally. - Look for known vulnerabilities with
npm audit.
Authentication and Authorization
React is a frontend framework. All authentication and authorization decisions must be enforced on the server. The client can be modified by the user.
// This is NOT security. It is UX.
{user.role === 'admin' && <AdminPanel />}
// The server must verify the user's role for every API request
// that the AdminPanel makes.
Never rely on client-side route guards for security. They prevent accidental access, not intentional attacks.
Dependency Security
React projects use the npm ecosystem. Apply all the standard JavaScript dependency security practices:
- Commit
package-lock.json - Use
npm ciin CI - Run
npm auditin your pipeline - Review new dependency additions carefully
- Generate SBOMs with CycloneDX
How Safeguard.sh Helps
Safeguard.sh monitors the full dependency tree of your React applications. Every npm package, every transitive dependency, every version. It correlates your deployed dependencies against live vulnerability feeds and alerts you when new CVEs affect your frontend applications. For teams running multiple React apps with shared component libraries, Safeguard.sh provides the cross-application visibility to ensure a vulnerability in a shared dependency is caught and remediated across every app, not just the one where it was first noticed.