React Security Best Practices: How to Secure Your React App in 2026
React powers some of the most popular web applications in the world — from dashboards and SaaS tools to AI-powered apps built with Lovable, Cursor, and Bolt. But React's flexibility comes with responsibility: the framework prevents some attacks by default, but leaves many security decisions entirely up to you.
If you're building with React (especially with AI assistance), this guide covers every security best practice you need to know — from XSS prevention to API key management, authentication patterns, and the mistakes we see in 98% of the React apps we scan.
React's Built-In Security (And Where It Stops)
React includes one critical security feature out of the box: automatic output encoding. When you render data with JSX, React escapes it by default:
// Safe — React escapes the HTML automatically
const userInput = '<script>alert("xss")</script>';
return <div>{userInput}</div>;
// Renders as text, not as executable HTML
This single feature prevents the majority of XSS (Cross-Site Scripting) attacks. But React's protection stops there. Everything else — authentication, authorization, API security, header configuration, dependency management — is your responsibility.
The 10 Most Common React Security Vulnerabilities
Based on scanning thousands of React applications with ClearAudit, here are the vulnerabilities we find most often:
1. Dangerous Use of dangerouslySetInnerHTML
React named it "dangerously" for a reason. This prop bypasses React's automatic XSS protection:
// ❌ DANGEROUS — Never use with untrusted content
<div dangerouslySetInnerHTML={{ __html: userGeneratedContent }} />
The fix: If you must render HTML (like blog content from a CMS), sanitize it first:
Even better: Use a Markdown renderer like react-markdown instead of raw HTML. It's safer by design and gives you more control over what elements are allowed.
2. API Keys Exposed in Frontend Code
This is the #1 vulnerability we find in AI-built React apps. Your React bundle is public — anyone can view the source code. Yet we constantly see:
// ❌ NEVER DO THIS
const STRIPE_SECRET_KEY = "sk_live_abc123...";
const DATABASE_PASSWORD = "supersecret";
const OPENAI_API_KEY = "sk-proj-abc123...";
What belongs in the frontend:
Publishable/anon keys designed for public use (Stripe publishable key, Supabase anon key)
Public API endpoints
What NEVER belongs in the frontend:
Secret API keys
Database credentials
JWT signing secrets
Any key with write/admin access
The fix: Move secret operations to a backend function (edge function, API route, serverless function) and call it from your React app.
3. Missing Security Headers
Your React app is served by a web server (Nginx, Vercel, Netlify, Cloudflare, etc.). That server needs to send security headers with every response:
Most React apps we scan are missing all of them. These headers prevent:
XSS attacks (CSP)
Protocol downgrade attacks (HSTS)
MIME type confusion attacks (X-Content-Type-Options)
Clickjacking (X-Frame-Options)
Information leakage (Referrer-Policy)
4. Insecure Authentication Patterns
We see these authentication mistakes constantly in React apps:
// ❌ WRONG — Storing auth state in localStorage
localStorage.setItem('isAdmin', 'true');
localStorage.setItem('userRole', 'admin');
// ❌ WRONG — Client-side role checking
if (localStorage.getItem('isAdmin') === 'true') {
showAdminPanel();
}
Why this is dangerous: Anyone can open DevTools and set isAdmin to true. Client-side checks are for UI convenience only — never for actual authorization.
The fix: Always verify authorization server-side. Use your backend to check roles on every protected API call:
// ✅ CORRECT — Server verifies the JWT and checks roles
const { data, error } = await supabase
.from('admin_data')
.select('*');
// RLS policies on the server enforce who can access this data
5. Unvalidated User Input
React escapes output, but it doesn't validate input. If you're sending user input to your backend without validation, you're vulnerable to:
SQL injection (if your backend constructs queries from user input)
6. Outdated Dependencies with Known Vulnerabilities
Your node_modules folder contains hundreds of packages, each with their own dependencies. Any one of them could have a known vulnerability:
# Check your project right now
npm audit
Best practices:
Run npm audit before every deployment
Use tools like Dependabot or Renovate for automated updates
Don't ignore high/critical severity warnings
Pin major versions to avoid breaking changes
7. Insecure Direct Object References (IDOR)
This happens when your React app constructs API calls using user-controllable IDs:
// ❌ VULNERABLE — User can change the ID to access other users' data
const fetchProfile = async (userId) => {
return fetch(`/api/users/${userId}/profile`);
};
The fix: Your backend must verify that the authenticated user has permission to access the requested resource. In Supabase, this means Row Level Security (RLS) policies:
-- Users can only read their own profile
CREATE POLICY "Users read own profile" ON profiles
FOR SELECT USING (auth.uid() = user_id);
8. Missing CSRF Protection
If your React app makes state-changing requests to your own backend (not a third-party API), you need CSRF protection. Modern approaches include:
SameSite cookies: Set SameSite=Strict or SameSite=Lax on session cookies
CSRF tokens: Include a server-generated token in forms and verify it on the backend
Custom headers: Require a custom header like X-Requested-With that browsers won't send in cross-origin requests
9. Sensitive Data in React State / URL
Don't put sensitive data where it can be observed:
// ❌ BAD — Sensitive data in URL (visible in browser history, logs, referrers)
navigate(`/checkout?card=4242424242424242&cvv=123`);
// ❌ BAD — Sensitive data in React state that persists in DevTools
const [creditCard, setCreditCard] = useState('4242...');
// ✅ GOOD — Use secure, httpOnly cookies or encrypted sessions for sensitive data
10. Missing Error Boundaries and Information Leakage
When your React app crashes, what does the user see? In development mode, React shows detailed error messages with stack traces, file paths, and component names. If this leaks to production:
// ❌ Error messages that leak internal details
catch (error) {
setError(`Database error: ${error.message}`);
// Might show: "relation 'users' does not exist" — reveals your schema
}
// ✅ Generic error messages in production
catch (error) {
console.error(error); // Log internally
setError("Something went wrong. Please try again.");
}
The Supabase + React Security Checklist
Since many React apps (especially those built with Lovable) use Supabase as their backend, here are Supabase-specific security practices:
Row Level Security (RLS) Is Non-Negotiable
Every table in your Supabase project should have RLS enabled with appropriate policies. The anon key in your React bundle gives anyone access to your Supabase API — RLS is what stops them from reading or writing data they shouldn't.
-- Enable RLS on every table
ALTER TABLE public.my_table ENABLE ROW LEVEL SECURITY;
-- Example: Users can only CRUD their own data
CREATE POLICY "Users manage own data" ON my_table
FOR ALL USING (auth.uid() = user_id);
Protect Your Anon Key
Your Supabase anon key is designed to be public — it's in your frontend bundle by design. But it's only safe if:
Client-side validation (Zod schemas, form validation) improves UX but provides zero security. An attacker can bypass your React UI entirely and call your API directly:
# Attacker bypasses your beautiful React form
curl -X POST https://your-app.supabase.co/rest/v1/orders \
-H "apikey: your-anon-key" \
-d '{"amount": -999, "product_id": "doesnt-exist"}'
Use database constraints, RLS policies, and edge functions for real validation.
Environment Variables in React
React (via Vite or Create React App) exposes environment variables to the frontend — which means they're in your JavaScript bundle:
# Vite — anything starting with VITE_ is PUBLIC
VITE_SUPABASE_URL=https://abc.supabase.co # ✅ OK — designed to be public
VITE_SUPABASE_ANON_KEY=eyJ... # ✅ OK — designed to be public
VITE_API_SECRET=sk_live_... # ❌ EXPOSED — this is now public!
# Non-VITE_ prefixed variables are NOT included in the bundle
DATABASE_URL=postgresql://... # ✅ Safe — not in frontend bundle
STRIPE_SECRET_KEY=sk_live_... # ✅ Safe — not in frontend bundle
Rule: If it starts with VITE_ (or REACT_APP_ in CRA), treat it as public information.
Content Security Policy for React Apps
CSP is the single most effective header for preventing XSS. Here's a practical CSP for a typical React + Supabase app:
Start strict and loosen only as needed. Every 'unsafe-*' directive you add weakens your protection.
Automated Security Scanning for React Apps
Manual code review catches some vulnerabilities, but automated scanning catches the ones you'd never think to look for. A good security scanner for React apps should check:
120+ security signals across headers, SSL, authentication, and more
API key exposure in your JavaScript bundle
Security header completeness and configuration
SSL/TLS configuration strength
Known vulnerability detection in your tech stack
SEO security signals that affect your Google ranking
ClearAudit was built specifically for this — we scan React apps (including Lovable, Cursor, and Bolt-built apps) and give you a prioritized list of what to fix, with copy-paste remediation prompts you can feed directly to your AI coding tool.
Security Checklist for React Developers
Before deploying any React app, run through this checklist:
Authentication & Authorization
Never store roles or permissions in localStorage
Verify authorization server-side on every protected request
Use secure, httpOnly cookies for session management when possible
Implement proper logout that invalidates sessions
Data Protection
No secret API keys in frontend code
All sensitive operations happen in backend functions
User input is validated on both client and server
Error messages don't leak internal details in production
Infrastructure
All security headers are configured (CSP, HSTS, X-Frame-Options, etc.)
SSL/TLS is properly configured with strong ciphers
Dependencies are up to date with no known vulnerabilities
CORS is configured to allow only expected origins
Supabase-Specific
RLS is enabled on every table
RLS policies are tested (try accessing data as different users)
No service_role key in frontend code
Database functions with SECURITY DEFINER are reviewed carefully
Conclusion
React gives you a head start with automatic output encoding, but that's just the beginning. The majority of React security vulnerabilities we find — exposed API keys, missing headers, client-side authorization, insecure authentication — are all preventable with the practices in this guide.
The fastest way to know where your React app stands? Run an automated scan. It takes 60 seconds and catches the vulnerabilities you didn't know you had.
Is your React app secure?Run a free security scan and get your security grade in 60 seconds. ClearAudit checks 120+ security signals across your headers, SSL, authentication, and more — with AI-powered fix prompts you can paste directly into your coding tool.