React Security Checklist: 10 Vulnerabilities to Fix Before Launch
Essential React security guide. Fix XSS via dangerouslySetInnerHTML, stop leaking secrets in bundles, secure your API calls, and more.
By Daniel A · Kraftwire Software
· 9 min readKey Takeaway
React applications face unique security challenges because of their client-side nature. Everything in your React code is visible to users, which means secrets, business logic, and access control must be handled carefully. This checklist covers the most important security practices for React apps.
Why React Security Is Different
React runs in the browser. That means every piece of JavaScript you ship is accessible to anyone who visits your page. They can open DevTools, read your source code, inspect network requests, and modify the application state.
This is not a flaw in React. It is how browsers work. But it means you need to think about security differently than you would with server-rendered applications.
Never Store Secrets in React Code
This is the most critical rule. Never put API keys, database credentials, encryption keys, or any sensitive value in your React code. Not in environment variables prefixed with VITE_ or REACT_APP_. Not in configuration files. Not in comments.
Environment variables that start with VITE_ (in Vite projects) or REACT_APP_ (in Create React App projects) are bundled into the JavaScript that gets sent to the browser. They are not secret.
What Belongs in the Frontend
Publishable API keys (like Stripe's publishable key)
Public configuration (API base URLs, feature flags)
Analytics tracking IDs
What Does Not Belong in the Frontend
Secret API keys
Database connection strings
Encryption keys
Admin credentials
Internal service URLs
How to Handle Secrets
Use server-side functions or a backend API to handle operations that require secrets. Your React frontend calls the server, and the server uses the secret to make the actual API call.
// Bad: Secret key in frontend code
const response = await fetch("https://api.example.com/data", {
headers: { "Authorization": "Bearer sk_secret_key_here" }
});
// Good: Call your own backend which holds the secret
const response = await fetch("/api/get-data");
Cross-Site Scripting (XSS) Prevention
React provides built-in XSS protection by escaping values in JSX. When you render a variable inside curly braces, React automatically escapes HTML characters so they display as text instead of being interpreted as HTML.
// Safe: React escapes this automatically
const userInput = '<script>alert("xss")</script>';
return <div>{userInput}</div>;
// Renders as text, not as a script tag
The dangerouslySetInnerHTML Trap
React's protection breaks down when you use dangerouslySetInnerHTML. This prop tells React to insert raw HTML without escaping. If the HTML comes from user input, you have an XSS vulnerability.
// Dangerous: raw HTML from user input
return <div dangerouslySetInnerHTML={{ __html: userComment }} />;
If you must render HTML content, sanitize it first using a library like DOMPurify.
import DOMPurify from "dompurify";
const sanitized = DOMPurify.sanitize(userComment);
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
Other XSS Vectors in React
Setting href attributes with user input (javascript: URLs)
Dynamically creating script tags
Using eval() or Function() with user data
Rendering user content in SVG elements
// Dangerous: user-controlled href
<a href={userProvidedUrl}>Click here</a>
// Safe: validate the URL first
const isValidUrl = (url: string) => {
try {
const parsed = new URL(url);
return ["http:", "https:"].includes(parsed.protocol);
} catch {
return false;
}
};
Authentication and Route Protection
Protecting routes in React is a user experience feature, not a security feature. A client-side route guard prevents users from seeing a page, but it does not prevent them from accessing the data behind it.
Client-Side Route Protection
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { user, loading } = useAuth();
if (loading) return <LoadingSpinner />;
if (!user) return <Navigate to="/login" />;
return <>{children}</>;
}
This is fine for UX, but you must also protect the data on the server side. Every API call that returns sensitive data should verify the user's authentication token.
State Management Security
Be careful about what you store in React state, context, or global stores like Redux. While state is not directly visible in the URL, it can be inspected through React DevTools.
What Not to Store in State
Full user objects with sensitive fields
Authentication tokens (use httpOnly cookies instead when possible)
Decrypted sensitive data
API responses that include fields the user should not see
Dependency Security
React applications typically have hundreds of npm dependencies. Each one is a potential security risk. A single compromised package can steal user data, inject malicious code, or mine cryptocurrency in your users browsers.
How to Stay Safe
Run npm audit regularly and fix high/critical vulnerabilities
Use lockfiles (package-lock.json or yarn.lock) to pin exact versions
Review the changelog before updating major versions
Remove unused dependencies
Be cautious with packages that have few downloads or recent ownership changes
Form Security
Forms in React need both client-side and server-side validation. Client-side validation provides instant feedback. Server-side validation provides actual security.
Common Form Security Issues
No server-side validation (relying only on React form validation)
Missing CSRF protection on form submissions
File upload forms that accept any file type
Forms that submit sensitive data over HTTP instead of HTTPS
Content Security Policy
A Content Security Policy (CSP) header tells the browser which resources it can load. This prevents XSS attacks by blocking unauthorized scripts, even if an attacker manages to inject HTML into your page.
For React apps, you typically need to allow inline styles (or use a nonce) and your own script bundles. Start with a restrictive policy and relax it as needed.
Secure Communication
All API calls from your React app should use HTTPS. This encrypts data in transit and prevents man-in-the-middle attacks. Most hosting platforms enforce HTTPS by default, but verify that your API endpoints also use HTTPS.
Fetch Security
// Always use HTTPS for API calls
const response = await fetch("https://api.example.com/data", {
credentials: "same-origin", // Only send cookies to same origin
headers: {
"Content-Type": "application/json",
},
});
Your React Security Checklist
Secrets
No API keys or credentials in frontend code
Environment variables reviewed for accidental exposure
Secrets handled through server-side functions
XSS Prevention
No unvalidated use of dangerouslySetInnerHTML
User-provided URLs validated before rendering
No eval() or Function() with user data
Authentication
Route protection paired with server-side validation
Session tokens stored securely
Authentication state verified on every sensitive API call
Dependencies
npm audit run regularly
Lockfiles committed to version control
Unused packages removed
Communication
All API calls use HTTPS
Credentials policy set appropriately
Security headers configured
Automate Your React Security Review
Manual review catches some issues, but automated scanning catches the patterns that repeat across your codebase. SimplyScan checks your React application for exposed secrets, missing headers, and common misconfigurations in minutes.