React Security Best Practices: XSS Prevention, Auth, and Data Protection

Share

TL;DR

The #1 React security best practice is avoiding dangerouslySetInnerHTML and validating all user input. React escapes content by default, but security still requires attention. Never store secrets in client code, and use HttpOnly cookies for auth tokens. Following these practices prevents 76% of frontend security vulnerabilities. Total time: approximately 35 minutes to implement all 8 best practices.

"React protects you from XSS by default, but one dangerouslySetInnerHTML can undo it all. Trust React's escaping, distrust user input."

React's Built-in XSS Protection

React automatically escapes values rendered in JSX, preventing most XSS attacks:

React automatically escapes content
// Safe: React escapes the content
function UserComment({ comment }) {
  return <p>{comment}</p>;
}

// Even if comment contains:
// "<script>alert('XSS')</script>"
// React renders it as text, not HTML

However, there are ways to bypass this protection. Avoid them unless absolutely necessary.

Best Practice 1: Avoid dangerouslySetInnerHTML 3 min

The dangerouslySetInnerHTML prop bypasses React's XSS protection:

Dangerous: Avoid unless necessary
// DANGEROUS: Renders HTML directly
function UnsafeComponent({ htmlContent }) {
  return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
}

If you must render HTML, sanitize it first:

Safer: Sanitize before rendering
import DOMPurify from 'dompurify';

function SaferHtmlComponent({ htmlContent }) {
  const sanitizedHtml = DOMPurify.sanitize(htmlContent, {
    ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a'],
    ALLOWED_ATTR: ['href'],
  });

  return <div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />;
}

Better alternative: Use a markdown library like react-markdown instead of rendering raw HTML. It provides structured, safe rendering of formatted content.

Best Practice 2: Validate All User Input 5 min

Never trust user input. Validate on both client and server:

Input validation with Zod and React Hook Form
import { z } from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';

const userSchema = z.object({
  email: z.string().email('Invalid email'),
  name: z.string()
    .min(2, 'Name too short')
    .max(100, 'Name too long')
    .regex(/^[a-zA-Z\s]+$/, 'Name can only contain letters'),
  website: z.string().url().optional().or(z.literal('')),
});

function ProfileForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(userSchema),
  });

  const onSubmit = async (data) => {
    // Data is validated - safe to send to API
    await updateProfile(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email')} />
      {errors.email && <span>{errors.email.message}</span>}
      {/* ... */}
    </form>
  );
}

Best Practice 3: Secure URL Handling 3 min

URLs from user input can be used for attacks:

Validate URLs before use
function SafeLink({ url, children }) {
  const isValidUrl = (urlString) => {
    try {
      const url = new URL(urlString);
      // Only allow http and https protocols
      return ['http:', 'https:'].includes(url.protocol);
    } catch {
      return false;
    }
  };

  if (!isValidUrl(url)) {
    return <span>{children}</span>; // Render as text if invalid
  }

  return (
    <a
      href={url}
      target="_blank"
      rel="noopener noreferrer"
    >
      {children}
    </a>
  );
}

Watch out for: javascript: URLs can execute code. Never allow user-provided URLs in href without validation. The javascript: protocol bypasses React's XSS protection.

Best Practice 4: Secure Authentication State 5 min

Handle authentication tokens securely:

Secure auth token handling
// WRONG: Storing token in localStorage (vulnerable to XSS)
localStorage.setItem('token', authToken);

// BETTER: Use HttpOnly cookies (set by server)
// The cookie is automatically sent with requests
// and cannot be accessed by JavaScript

// For API calls, let the browser handle cookies:
async function fetchUserData() {
  const response = await fetch('/api/user', {
    credentials: 'include', // Send cookies
  });
  return response.json();
}

// If you must use tokens in memory (SPA with separate API):
// Store in memory, not localStorage
const authStore = {
  token: null,
  setToken(token) { this.token = token; },
  getToken() { return this.token; },
  clearToken() { this.token = null; },
};

Best Practice 5: Protect Against CSRF 5 min

If your backend uses cookies for authentication, implement CSRF protection:

CSRF token handling in React
// Get CSRF token from meta tag or cookie
function getCsrfToken() {
  return document.querySelector('meta[name="csrf-token"]')?.content;
}

// Include in API calls
async function secureApiCall(endpoint, data) {
  const response = await fetch(endpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': getCsrfToken(),
    },
    credentials: 'include',
    body: JSON.stringify(data),
  });
  return response.json();
}

Best Practice 6: Environment Variables 2 min

React apps run in the browser. All environment variables are exposed:

Environment variable safety
// In React (Create React App, Vite):
// All REACT_APP_ or VITE_ prefixed vars are bundled into the app

// SAFE: Public configuration
const apiUrl = import.meta.env.VITE_API_URL;
const publicKey = import.meta.env.VITE_STRIPE_PUBLIC_KEY;

// NEVER DO THIS: These would be exposed to users
// const apiSecret = import.meta.env.VITE_API_SECRET;  // WRONG!
// const dbPassword = import.meta.env.VITE_DB_PASS;    // WRONG!

// Keep secrets on your backend server, not in React

Best Practice 7: Secure Dependencies 5 min

Third-party packages can introduce vulnerabilities:

Dependency Security Checklist:

  • Run npm audit regularly and fix vulnerabilities
  • Use npm audit --fix for automatic patches
  • Review packages before installing (check downloads, maintenance)
  • Keep dependencies updated, especially security patches
  • Use lock files (package-lock.json) for reproducible builds
  • Consider tools like Snyk or Dependabot for monitoring

Best Practice 8: Secure API Calls 7 min

Handle API errors and data safely:

Secure API call patterns
async function fetchData(endpoint) {
  try {
    const response = await fetch(endpoint, {
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
      },
    });

    if (response.status === 401) {
      // Handle unauthorized - redirect to login
      window.location.href = '/login';
      return null;
    }

    if (!response.ok) {
      // Log error but don't expose details to user
      console.error('API error:', response.status);
      throw new Error('Request failed');
    }

    return await response.json();
  } catch (error) {
    // Generic error message for users
    // Log full error for debugging
    console.error('Fetch error:', error);
    throw new Error('Unable to load data. Please try again.');
  }
}

Common React Security Mistakes

MistakeRiskPrevention
Using dangerouslySetInnerHTMLXSS attacksSanitize with DOMPurify or avoid
Storing tokens in localStorageToken theft via XSSUse HttpOnly cookies
Secrets in env varsCredential exposureKeep secrets on backend only
Unsanitized URLs in hrefXSS via javascript: URLsValidate URL protocol
Missing input validationInjection attacksValidate with Zod or similar

Does React prevent XSS automatically?

React escapes content rendered in JSX, preventing most XSS. However, dangerouslySetInnerHTML, javascript: URLs, and some edge cases can still allow XSS. Always validate user input and avoid rendering raw HTML.

Where should I store auth tokens in React?

Prefer HttpOnly cookies set by your server. If you must use tokens in a SPA with a separate API, store them in memory (React state/context) rather than localStorage. Tokens in localStorage are vulnerable to XSS.

Can I put API keys in my React app?

Only publishable/public keys that are designed for client-side use (like Stripe publishable keys). Secret keys must stay on your server. Everything in a React app is visible to users.

How do I handle authentication in React?

Use established libraries like NextAuth, Auth0, or Firebase Auth rather than building custom auth. If using JWTs, validate them on your server for every request, not just on the client.

Verify Your React Security

Scan your React project for security issues and vulnerabilities.

Start Free Scan
Best Practices

React Security Best Practices: XSS Prevention, Auth, and Data Protection