
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.

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
May 7, 2026

Is Nuxt something for “me”?
Using Nuxt vs Not Using Nuxt: A side-by-side comparison of building the same features in plain Vue and Nuxt, so you can see exactly what the framework gives you.
Reza Baar
May 6, 2026

RxJS to Angular Signals: Patterns, Pitfalls, and Practical Tips
Migrating from RxJS to Signals in Angular? This guide breaks down common patterns, practical tips, and step-by-step strategies to help you transition smoothly from reactive streams to a signals-based architecture.
Alain Chautard
May 5, 2026
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.
