
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
May 7, 2026
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.
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 {}.
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.
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
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.
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:
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 (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 defaultscript-src 'self': Only allow scripts from the same origin (blocks injected inline scripts)object-src 'none': Blocks plugins like Flashframe-ancestors 'none': Prevents your page from being embedded in iframes (clickjacking protection)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.
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.
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:
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.

Middleware: What It Is, How It Chains, and When to Write Your Own
Middleware is one of Laravel’s most tested certification topics because it sits at the core of the request lifecycle. This article goes beyond basic syntax to explain how middleware works internally, how the pipeline pattern processes requests, what happens when $next is skipped, and why some middleware never executes. If you want to truly understand Laravel middleware rather than just use it, this is where to start.
Steve McDougall
May 28, 2026

Rolldown and Vite 8: What Changed
Vite 8 replaced both esbuild and Rollup with Rolldown. Here's what that means for your Vue project in practice.
Reza Baar
May 27, 2026

Closures Explained: How Functions Remember Their Scope
A function in JavaScript remembers the scope it was created in, even after that scope has finished executing. Learn what closures are, why the loop bug happens, and how to use them in practice.
Martin Ferret
May 26, 2026