Home
Back to blog

CVE-2025-55182 Is What Happens When You Hide The Network Boundary

·Kevin Karsopawiro
security
react
nextjs
javascript

On December 3rd, the React team disclosed CVE-2025-55182: a CVSS 10.0 unauthenticated remote code execution vulnerability in React Server Components. An attacker can craft a malicious HTTP request to any Server Function endpoint that, when deserialized by React, achieves arbitrary code execution on your server.

No authentication required. Just send the payload.

If you're running React 19.0 through 19.2.0 with Server Functions, stop reading and go upgrade. Next.js users: 15.0.5, 15.1.9, 15.2.6, 15.3.6, 15.4.8, 15.5.7, or 16.0.7. Everyone else: react-server-dom-webpack/parcel/turbopack version 19.0.1, 19.1.2, or 19.2.1.

Now let's talk about why this was inevitable.

We've Reinvented PHP

Here's the thing: a critical deserialization RCE is not a new class of vulnerability. It's one of the oldest tricks in the book. PHP's unserialize() function was so notorious for enabling RCE that "never unserialize untrusted input" became a commandment. That was fifteen years ago.

And here we are in 2025, mass-adopting a framework where you write "use server" and your function becomes a public endpoint that deserializes arbitrary payloads from the internet.

"use server";

export async function updateProfile(data: FormData) {
  // This is a public HTTP endpoint
  // That accepts serialized data from anyone on the internet
  // And deserializes it on your server
  // What could go wrong?
}

We didn't learn from PHP. We just added better syntax highlighting.

Surprised Pikachu:

The Magic That Betrays You

The entire value proposition of React Server Components is making the network boundary invisible. Call a function. The framework figures out if it runs on the client or server. Seamless. Magical.

But here's the problem: the network boundary isn't just a performance concern. It's a security boundary. When you hide it, you hide the attack surface.

In the old world, if you wanted server-side functionality, you wrote an API route:

// pages/api/update-profile.ts
export default async function handler(req: Request, res: Response) {
  // Obviously an HTTP endpoint
  // Obviously accepts untrusted input
  // Obviously needs validation and authentication
}

The explicitness was a feature. You knew you were writing an endpoint. You knew the input was untrusted. Your security intuitions worked.

With Server Functions:

"use server";

export async function updateProfile(formData: FormData) {
  // Looks like a regular function
  // Called like a regular function
  // Your intuitions say "this is just a function"
}

It's not just a function. It's an HTTP endpoint that accepts POST requests with serialized payloads from anywhere on the internet. But nothing in the syntax tells you that. The abstraction hides exactly the thing you need to think about for security.

The Deserialization Problem

The specific vulnerability in CVE-2025-55182 is in React's payload deserialization. When a Server Function is called, React serializes the arguments, sends them over HTTP, and deserializes them on the server. The deserialization had a flaw that allowed arbitrary code execution.

This is PHP's unserialize() all over again. Same class of bug. Same root cause: trusting serialized data from untrusted sources.

The React team will patch this specific bug. But the architecture remains. Server Functions will always need to deserialize data from the network. That code path will always be a high-value target. There will be more CVEs.

When you create a framework where every "use server" function becomes an RPC endpoint accepting serialized payloads, you've created a massive attack surface. The question was never "if" there would be a critical deserialization bug. It was "when."

The Mental Model Mismatch

Here's what makes this particularly insidious: the developers using Server Functions often don't realize they're writing public endpoints.

Ask a developer what this code does:

"use server";

export async function deleteUser(userId: string) {
  await db.users.delete({ where: { id: userId } });
}

They'll say "it deletes a user from the database." What they probably won't say is "it exposes an unauthenticated HTTP endpoint that accepts a user ID from anyone on the internet and deletes that user."

But that's what it does. The "use server" directive doesn't add authentication. It doesn't add authorization. It just makes the function callable over HTTP. Any authentication or authorization is your responsibility.

This is obvious once you know it. But the syntax actively obscures it. A function with "use server" looks and feels like a regular function. Your instincts from writing regular functions don't apply.

The Historical Pattern

We've been here before.

PHP (2000s): Mix HTML and server code freely! Just write and you're on the server. Result: SQL injection, XSS, and RCE epidemics. Entire categories of vulnerabilities that defined a decade of web security.

Ruby on Rails (2013): Automatic parameter deserialization! Just accept params and Rails handles the rest. Result: CVE-2013-0156, a YAML deserialization RCE. CVSS 10.0.

Java (2015-forever): Serialization is convenient! Just deserialize that data. Result: Endless stream of deserialization RCEs. Log4Shell. Spring4Shell. The gift that keeps giving.

React Server Functions (2025): Seamless server-client boundary! Just write "use server". Result: CVE-2025-55182. CVSS 10.0.

The pattern is: make dangerous things convenient, hide the danger behind abstraction, watch developers footgun themselves at scale.

What "use server" Should Have Been

Here's a thought experiment. What if "use server" was named to reflect what it actually does?

"expose as unauthenticated http endpoint that deserializes untrusted input";

export async function updateProfile(formData: FormData) {
  // Now your brain is in the right mode
}

Verbose? Sure. But you'd never forget to add authentication.

The naming of "use server" emphasizes where the code runs. But the security-critical property is what the code accepts: arbitrary serialized input from the public internet. The abstraction points at the wrong thing.

Protecting Yourself

Beyond the immediate patch, here's how to think about Server Functions going forward:

Treat Every Server Function As A Public API

Because that's what it is. Every function marked "use server" needs:

"use server";

import { auth } from '@/lib/auth';
import { z } from 'zod';

const UpdateProfileSchema = z.object({
  name: z.string().min(1).max(100),
  bio: z.string().max(500).optional(),
});

export async function updateProfile(formData: FormData) {
  // 1. Authentication
  const session = await auth();
  if (!session) {
    throw new Error('Unauthorized');
  }

  // 2. Input validation (don't trust the types)
  const rawData = Object.fromEntries(formData);
  const data = UpdateProfileSchema.parse(rawData);

  // 3. Authorization
  if (session.user.id !== data.userId) {
    throw new Error('Forbidden');
  }

  // NOW you can use the data
  await db.users.update({
    where: { id: session.user.id },
    data: { name: data.name, bio: data.bio }
  });
}

Yes, this is more code than just writing the database call. That's the point. The "convenience" of Server Functions was hiding necessary work, not eliminating it.

Don't Trust TypeScript Types At Runtime

TypeScript types are compile-time only. They don't exist at runtime. A Server Function typed to accept string will happily receive whatever the attacker sends:

"use server";

// TypeScript says this accepts a string
export async function getUser(id: string) {
  // At runtime, id could be anything
  // An object, an array, a malicious payload
  // TypeScript can't protect you here
}

Validate inputs with a runtime validator like Zod. Always.

Audit Your Server Functions

Go through your codebase right now. Find every "use server" directive. For each one, ask:

  1. Is there authentication?
  2. Is there authorization?
  3. Is the input validated at runtime?
  4. What happens if the input is malicious?

You will probably find problems.

The Broader Lesson

React Server Components are a genuinely interesting technology. The ability to blur the server-client boundary enables new patterns and can simplify certain architectures.

But that blurring has a cost. When the network boundary is invisible, so is the attack surface. When calling a server function looks like calling a regular function, developers don't apply their security intuitions.

This CVE will be patched. The next one will be found and patched too. But the fundamental tension remains: the more you hide the network boundary, the more you hide the security boundary.

The React team isn't incompetent. They're fighting against a fundamental tradeoff. Making RPC feel like function calls is inherently dangerous, no matter how carefully you implement it. The abstraction is the attack surface.

The Bottom Line

CVE-2025-55182 is a critical RCE that you should patch immediately. But the patch doesn't fix the underlying issue: Server Functions are public endpoints accepting serialized input, and the syntax hides that completely.

We wanted PHP's convenience without PHP's problems. We got PHP's convenience with PHP's problems, plus a build step.

The network boundary exists. Pretending it doesn't has consequences. This CVE is one of them.

Patch your servers. Validate your inputs. And maybe be a little more skeptical the next time someone promises to make complexity disappear. It usually just moves somewhere you can't see it.

Like into a CVSS 10.0.