Securing a React application in 2026 means covering four surfaces: the UI (content injection), sessions (where tokens live), the API (server-side authorization and validation) and, if you use Next.js or another framework with React Server Components, the code that runs on the server. That last surface is the newest and the most critical: in December 2025, a Server Components flaw allowed remote code execution on servers with no authentication at all.
Most incidents don’t come from sophisticated attacks but from incomplete settings: unfiltered content that becomes a script, a token left in localStorage, CORS opened “just for testing”, a Server Action that doesn’t check permissions, a framework that was never updated. Here are the rules we apply on our React projects.

The risk map
Risks fall into six families:
- Injection and XSS: external text rendered as executable HTML is enough to hijack a session.
- Leaked secrets: any token JavaScript can reach (local storage, logs, URLs) will eventually be read by malicious code.
- Incomplete access control: authenticated doesn’t mean authorized; without checks close to the resource, a user reads or changes what isn’t theirs.
- Exposed server code: Server Actions and Server Components are public entry points, and data passed to client components is sent to the browser.
- Permissive CORS and headers: allowing any origin to read your private responses opens up your data.
- Dependency chain and framework: compromised, outdated or unpatched libraries. We cover this in a dedicated article: npm install and security.
UI: make injection harmless
React escapes text inserted in JSX: that’s a good default. It goes away in three cases: dangerouslySetInnerHTML, URLs built from external data (an href starting with javascript:), and code that manipulates the DOM directly.
If you must display HTML (rich text editor, CMS content), sanitize it before rendering with a proven library such as DOMPurify:
import DOMPurify from 'dompurify';
export function RichText({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html, { USE_PROFILES: { html: true } });
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
For links, only accept the expected protocols (https:, mailto:) and reject everything else.
Then add a Content-Security-Policy: it only allows your own script, style and image sources, blocks unsigned inline scripts (use a nonce when the framework supports it) and prevents your pages from being embedded in unauthorized iframes (frame-ancestors). Complete it with Referrer-Policy and Permissions-Policy.
Finally, keep the UI lean: no sensitive identifiers in URLs, no stack traces in production, error messages that help without revealing technical details.
Sessions: authenticate without exposing secrets
The React app shouldn’t handle secrets. Prefer session cookies flagged HttpOnly, Secure and SameSite: JavaScript can’t read them, they only travel over HTTPS and they limit cross-site requests.
If you use SSO (OpenID Connect), do the code exchange on the server. For stricter isolation, the Backend-for-Frontend (BFF) pattern puts a small server on the same domain as the app: it holds the tokens and talks to the API and the identity provider. The result: no tokens in the browser and simpler CORS.
On logout, invalidate the session on the server before clearing the client, to avoid “ghost” sessions.
React Server Components and Server Actions: the new attack surface
With React 19 and the frameworks that bundle it (Next.js with the App Router, React Router, Waku…), part of your React code runs on the server. Two mechanisms need special attention.
Server Components run on the server and send the browser the rendered result, along with the props of the client components they contain. Anything you pass as props to a client component is therefore serialized and visible in the browser.
Server Actions (React now calls them Server Functions) are server functions callable from the client. Each one becomes an HTTP entry point. Anyone can call it with any parameters, even if no button exposes it.
What December 2025 showed
On December 3, 2025, the React team published CVE-2025-55182, nicknamed React2Shell: unauthenticated remote code execution in the Server Components protocol, rated 10 out of 10 (CVSS). It affected versions 19.0.0 through 19.2.0 of react-server-dom-webpack, react-server-dom-parcel and react-server-dom-turbopack, and therefore Next.js 15 and 16 with the App Router. An app was exposed as soon as it supported Server Components, even without any declared Server Action. The flaw was actively exploited within days.
On December 11, two related vulnerabilities followed: a denial of service (CVE-2025-55184) and source code exposure of Server Functions (CVE-2025-55183). Up-to-date versions are 19.0.3, 19.1.4 and 19.2.3 or later. The pace hasn’t slowed: on September 22, 2026, Next.js shipped yet another out-of-band security update (16.3.6) for a code execution flaw in a dependency.
Earlier, in March 2025, CVE-2025-29927 allowed attackers to bypass Next.js middleware with a single HTTP header. Apps that only checked permissions in middleware were wide open.
The rules we apply
- Treat every Server Action as a public API route: authentication, permission checks on the resource and parameter validation inside the function itself.
- Never rely on middleware alone for authorization: it filters, it doesn’t protect data.
- Centralize data access in a server layer (Data Access Layer) that checks permissions and only returns the fields you need.
- Only pass minimal objects to client components (DTOs), never the full database record.
- Mark server modules with
import 'server-only': the build fails if a client component imports them. - Keep secrets out of the bundle: with Next.js, only variables prefixed
NEXT_PUBLIC_are sent to the browser; no secret should carry that prefix. - Update fast: follow React and framework security advisories and apply fixes in days, not months.
A simplified Server Action with Next.js and Zod (auth and db are your application’s own modules):
'use server';
import { z } from 'zod';
import { auth } from '@/lib/auth';
import { db } from '@/lib/db';
const Input = z.object({
invoiceId: z.string().uuid(),
note: z.string().min(1).max(500),
});
export async function addInvoiceNote(raw: unknown) {
const session = await auth();
if (!session) throw new Error('Not authenticated');
const { invoiceId, note } = Input.parse(raw);
const invoice = await db.invoice.findUnique({ where: { id: invoiceId } });
if (!invoice || invoice.companyId !== session.user.companyId) {
throw new Error('Access denied');
}
await db.invoiceNote.create({
data: { invoiceId, note, authorId: session.user.id },
});
}
And to prevent a data access module from being imported on the client:
// lib/invoices.ts
import 'server-only';
export async function getInvoiceSummary(id: string, companyId: string) {
// checks permissions and only returns the fields that are displayed
}
API and infrastructure: precise permissions, tight CORS, ongoing hygiene
Everything that matters is checked on the server. Validation in the React form serves the user experience; the validation that protects you is the server’s (types, sizes, formats), on every request.
Configure CORS as an allowlist: specific origins, explicit methods and headers, credentials only when needed, and never with *. Add rate limiting, quotas, timeouts and a maximum upload size. Enable HSTS, remove server banners, control caching of private responses and keep sensitive data out of logs (a correlation ID is enough to trace an incident).
A three-step path
Step 1: set the default protections. HTML sanitization and CSP, HttpOnly cookies, access control in every route and every Server Action, CORS allowlist, rate limiting, up-to-date React and framework versions.
Step 2: measure and fill the gaps. Track CSP violations, access denials, spikes in rejected requests, server errors. Adjust to reality: tighten a CORS rule, lower an upload limit, clean up an overly verbose log.
Step 3: maintain. Automated dependency updates (Dependabot, Renovate) with human review, monitoring of React and Next.js security advisories, regular review of policies (CSP, CORS, logs), tests of sensitive flows and logout.
Red flags to fix right away
- A version of React,
react-server-dom-*or Next.js older than the December 2025 fixes. - A Server Action with no session or permission check.
- Permissions checked only in middleware.
- A full database object passed as props to a client component.
- A secret in a
NEXT_PUBLIC_variable or in the bundle. - A token in localStorage or in the URL.
- CORS accepting
*or broad origins with cookies. dangerouslySetInnerHTMLwithout prior sanitization.
How we work
Securing a React app is no mystery: neutralize injection, keep secrets out of the browser, check permissions on the server at every entry point, and keep the framework up to date. The hard part is doing it everywhere, consistently, over time.
On our React and Next.js projects, that’s the Etixio tech lead’s job: they set these rules, check them in code review and make sure they are tested in continuous integration. That’s how we work, for example, on the React front end of a regulatory compliance SaaS.
Want an outside view of an existing application? We can start with a technical audit or make it part of a cybersecurity and compliance program. Tell us about your project.
FAQ
Does React protect against XSS out of the box?
Partly. React escapes text inserted in JSX, which neutralizes most injections. That protection goes away with dangerouslySetInnerHTML, with URLs built from external data (links, redirects) or with code that manipulates the DOM directly. In those cases, you need to sanitize the data and add a Content-Security-Policy.
Where should you store an authentication token in a React app?
Ideally nowhere JavaScript can reach. A session in an HttpOnly, Secure, SameSite cookie can’t be read by an injected script. To go further, a Backend-for-Frontend (BFF) keeps tokens on the server so the browser never sees the raw access token.
Are Server Actions secure by default?
No. A Server Action (or Server Function) is an HTTP entry point that anyone can call, even if no button in the UI exposes it. You need to authenticate the user, check their rights on the resource and validate parameters inside the function itself, just as you would for an API route.
What is the React2Shell vulnerability?
It is CVE-2025-55182, published on December 3, 2025: unauthenticated remote code execution in the React Server Components protocol (CVSS 10). It affected versions 19.0 through 19.2.0 of the react-server-dom-* packages and the frameworks that bundle them, including Next.js with the App Router. It was actively exploited. The only fix was to upgrade.
Is a React app without a server affected by Server Components vulnerabilities?
No. A purely client-side React app (for example built with Vite and served as static files) doesn’t use the Server Components protocol. Its main risks remain injection, session handling, the API it calls and its dependencies.


