WhatsAppDamascusSat – Thu·10:00 AM – 7:00 PM

Deserialization at the framework boundary

One RCE at CVSS 10.0, then six denial-of-service patches in eight months. When the contract is "accept whatever the format encodes", fixing one lever leaves the rest.

SecurityPublished 3 min read

The thing that reads your bytes

React Server Components move data between the server and the browser over a wire format called Flight. Server Functions — the ones you mark 'use server' — arrive as a request that the framework turns back into function arguments. Somewhere in that path is code whose whole job is to take bytes an attacker controls and reconstruct live JavaScript values from them.

That code is where CVE-2025-55182 lived. Unauthenticated remote code execution, CVSS 10.0, disclosed on 3 December 2025. It affected versions 19.0.0, 19.1.0, 19.1.1 and 19.2.0 of react-server-dom-webpack, react-server-dom-parcel and react-server-dom-turbopack, and it was fixed in 19.1.2 and 19.2.1. Proof-of-concept code was public within days.

Then it kept happening

The interesting part is not the RCE. It is the eight months after it:

  • CVE-2025-55183 — source code exposure. Fixed in 19.0.2, 19.1.3, 19.2.2.
  • CVE-2025-55184 — denial of service, same day.
  • CVE-2025-67779 — the fix for the previous one "was incomplete". Fixed in 19.0.3, 19.1.4, 19.2.3.
  • CVE-2026-23864 — the fixes, plural, "were incomplete". Fixed in 19.0.4, 19.1.5, 19.2.4.
  • CVE-2026-23869, then CVE-2026-23870, then CVE-2026-44907 in July 2026, fixed in 19.0.8, 19.1.9 and 19.2.8.

Six denial-of-service advisories against the same endpoint class, each one patching a case the last one missed. That is not carelessness. It is what happens when the attack surface is "whatever the format can express" rather than "the fields you declared".

Why the class is hard

Ordinary input validation works because you know the shape you want. A deserializer does not have that luxury: its contract is to accept whatever the protocol can encode, because legitimate clients send exactly that. Every feature of the format — nesting, references, lazy chunks, streamed values — is also a lever. Fixing one lever leaves the others.

So the number of advisories is a property of the design, not a verdict on the maintainers. React's response was fast and public each time, which is the behaviour you want. Plan for the next one anyway.

Are you exposed

You do not install these packages. A framework does, and that is why people miss it. Check the lockfile, not the manifest:

bash
npm ls react-server-dom-webpack react-server-dom-parcel \
       react-server-dom-turbopack

If nothing is returned, your app has no RSC runtime and none of this reaches you. If something is returned, the version that matters is the resolved one, however deep it sits under next, react-router or a bundler plugin.

What to actually do

Treat every 'use server' export as a public, unauthenticated endpoint. It is one. The function is reachable by anyone who can reach your site, whether or not any component in your app calls it, and whether or not the page that calls it is behind a login. The authorization check goes inside the function.

Validate the arguments at runtime. A TypeScript signature is a compile-time claim about a well-behaved caller. The caller here is the network.

Bound the input. Body size limits and a request timeout turn most denial-of-service findings from an outage into a rejected request, including the ones not yet published.

Patch on the release train, not on the headline. Seven advisories in eight months means subscribing to the source once beats reacting seven times.

The general shape

Whenever a framework offers to turn a request directly into a function call, it has put a deserializer on the boundary and given it a friendly name. Find that seam in whatever you run, and hold it to the rules above — the specific identifiers here will be stale within a year, and the seam will not.