Security in React Applications

Security in React Applications

Learn how to secure React apps: prevent XSS with DOMPurify, store tokens safely in HttpOnly cookies, validate server inputs with Zod, and configure Content Security Policy.

Aurora Scharff

Aurora Scharff

May 7, 2026

Security in React Applications

React provides built-in protection against common web vulnerabilities, but that protection only goes so far. As React applications take on more server-side responsibilities with Server Components and Server Functions, frontend developers need to think about security concerns that were traditionally handled by backend teams.

This article covers the security topics most relevant to React developers: XSS prevention and the escape hatches that bypass it, secure authentication and CSRF protection, server-side input validation with Zod, and Content Security Policy.

React's Built-in XSS Protection

Cross-Site Scripting (XSS) attacks inject malicious scripts into web pages. React protects against this by default — when you render a value in JSX, React automatically escapes it before inserting it into the DOM:

      function Comment({ text }) {
  return <p>{text}</p>;
}

<Comment text="<script>alert('XSS')</script>" />
// Renders as text: <script>alert('XSS')</script>
// The script does NOT execute

    

React converts special characters like <, >, and & into their HTML entity equivalents. This means user input rendered through JSX is safe by default — the browser treats it as text, not as HTML or JavaScript.

This protection applies to all JSX expressions: props, children, and any value placed inside {}.

dangerouslySetInnerHTML

React's auto-escaping is bypassed when you use dangerouslySetInnerHTML. This prop sets the innerHTML of a DOM element directly, which means any HTML — including <script> tags and event handlers — will be parsed and executed by the browser:

      function UnsafeComponent({ html }) {
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

// If html = '<img src="" onerror="alert(document.cookie)">'
// The attacker's script WILL execute

    

When you need to render HTML (for example, content from a CMS or a Markdown renderer), always sanitize it first. DOMPurify is the most widely used library for this:

      import DOMPurify from 'dompurify';

function BlogPost({ userContent }) {
  const cleanHTML = DOMPurify.sanitize(userContent);
  return <div dangerouslySetInnerHTML={{ __html: cleanHTML }} />;
}

    

DOMPurify strips dangerous tags, attributes, and inline event handlers while preserving safe HTML formatting. It also validates URI protocols to block javascript: and data: schemes.

The best approach is to avoid dangerouslySetInnerHTML entirely when possible. If you're rendering user-generated text, use React's default rendering. If you're rendering Markdown, convert it to React elements instead of HTML strings.

Secure Authentication and CSRF

Token Storage

A common mistake is storing authentication tokens in localStorage or sessionStorage. These are accessible to any JavaScript running on the page, which means a single XSS vulnerability gives an attacker access to the token:

      // ❌ Vulnerable to XSS — any script on the page can read this
localStorage.setItem('token', authToken);

// ❌ Same problem — sessionStorage is also accessible to JavaScript
sessionStorage.setItem('token', authToken);

    

Use HttpOnly cookies instead. The browser automatically includes them in requests, but JavaScript cannot read them — eliminating the XSS token theft vector entirely.

Configure cookies with these attributes on the server:

      Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict

    
  • HttpOnly: Prevents JavaScript access, blocking XSS-based cookie theft
  • Secure: Cookie is only sent over HTTPS connections
  • SameSite=Strict: Cookie is not sent with cross-site requests, preventing CSRF

CSRF Protection

Cross-Site Request Forgery (CSRF) attacks trick a user's browser into making requests to your application using their existing cookies. Even with SameSite=Strict, older browsers or specific configurations may require additional protection.

Include a CSRF token in state-changing requests:

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

    

The server generates and validates the token, ensuring the request came from your application and not from a third-party site.

Server-Side Input Validation

With Server Components and Server Functions, React applications now interact directly with databases and external services. This means frontend developers need to think about server-side concerns like SQL injection and authorization.

Client-side validation improves UX, but it provides no security — attackers can bypass any frontend check. Always validate on the server.

Zod provides type-safe schema validation that works well with Server Functions:

      'use server';

import { z } from 'zod';

const paymentSchema = z.object({
  email: z.string().email(),
  amount: z.number().positive(),
});

async function submitPayment(formData: FormData) {
  const session = await getSession();
  if (!session?.user) {
    return { error: 'Unauthorized' };
  }

  const result = paymentSchema.safeParse({
    email: formData.get('email'),
    amount: Number(formData.get('amount')),
  });

  if (!result.success) {
    return { error: result.error.flatten() };
  }

  await db.query(
    'INSERT INTO payments (user_id, email, amount) VALUES ($1, $2, $3)',
    [session.user.id, result.data.email, result.data.amount]
  );
}

    

This example follows three steps that every Server Function handling user input should include:

  1. Authenticate and authorize — verify the user has permission for this action
  2. Validate input — parse and validate with a schema before using the data
  3. Use parameterized queries — never concatenate user input into SQL strings

Parameterized queries (using $1, $2 placeholders) prevent SQL injection by ensuring user input is always treated as data, never as SQL commands.

Content Security Policy

Content Security Policy (CSP) is a browser security feature that restricts which resources can load on your page. It acts as a defense-in-depth layer — even if an attacker manages to inject a <script> tag, CSP can prevent it from executing.

CSP is configured via HTTP headers. A strict policy for a React application looks like this:

      Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  object-src 'none';
  frame-ancestors 'none'

    
  • default-src 'self': Only allow resources from the same origin by default
  • script-src 'self': Only allow scripts from the same origin (blocks injected inline scripts)
  • object-src 'none': Blocks plugins like Flash
  • frame-ancestors 'none': Prevents your page from being embedded in iframes (clickjacking protection)

Nonces for Inline Scripts

React applications often need inline scripts (for hydration or initial data). Instead of allowing all inline scripts with 'unsafe-inline' (which defeats the purpose of CSP), use nonces:

      Content-Security-Policy: script-src 'nonce-a1b2c3d4' 'strict-dynamic'

    

The server generates a unique nonce for each request and includes it in both the CSP header and the <script> tag:

      <script nonce="a1b2c3d4" src="/static/js/main.js"></script>

    

The 'strict-dynamic' directive allows scripts loaded by trusted scripts to execute, which is needed for code-split chunks in React applications.

Testing Policies

Use Content-Security-Policy-Report-Only to test a policy before enforcing it. This header logs violations without blocking resources, letting you identify issues before they break your application in production.

Conclusion

React's automatic JSX escaping handles the most common XSS vectors, but developers need to be aware of escape hatches like dangerouslySetInnerHTML and sanitize any raw HTML with DOMPurify. Beyond XSS, secure cookie attributes (HttpOnly, Secure, SameSite) and CSRF tokens protect authentication flows, while server-side validation with Zod and parameterized queries defend against injection attacks in Server Functions. Content Security Policy adds a final defense layer by restricting which resources the browser is allowed to load.

None of these measures work in isolation. A secure React application combines React's built-in protections with proper authentication patterns, server-side validation, and browser-level policies.


Sources:

More certificates.dev articles

Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.

Looking for Certified Developers?

We can help you recruit Certified Developers for your organization or project. The team has helped many customers employ suitable resources from a pool of 100s of qualified Developers.

Let us help you get the resources you need.

Contact Us
Customer Testimonial for Hiring
like a breath of fresh air
Everett Owyoung
Everett Owyoung
Head of Talent for ThousandEyes
(a Cisco company)