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:

// ✅ SAFE — Sanitize before rendering
import DOMPurify from 'dompurify';

const sanitized = DOMPurify.sanitize(userGeneratedContent);
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;

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:

What NEVER belongs in the frontend:

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:

Content-Security-Policy: default-src 'self'; script-src 'self'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()

Most React apps we scan are missing all of them. These headers prevent:

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:

// ❌ RISKY — No validation
const handleSearch = async (query) => {
  const res = await fetch(`/api/search?q=${query}`);
};

// ✅ SAFER — Validate and encode
const handleSearch = async (query) => {
  const sanitized = query.replace(/[^\w\s-]/g, '').slice(0, 100);
  const res = await fetch(`/api/search?q=${encodeURIComponent(sanitized)}`);
};

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:

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:

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:

  1. RLS is enabled on every table
  2. RLS policies are correct and tested
  3. No service_role key is exposed in the frontend
  4. Database functions use SECURITY DEFINER carefully

Read our deep dive: Your Supabase App Is Leaking Data

Validate on the Server

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:

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://*.supabase.co;
  font-src 'self';
  frame-ancestors 'none';

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:

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

Data Protection

Infrastructure

Supabase-Specific

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.

Related reading