React2Shell: how a deserialization flaw took React Server Components to a 10/10
by detektd team

On December 3, 2025, Meta published CVE-2025-55182, a flaw in React Server Components rated 10.0 out of 10 in CVSS, the maximum. Discovered and reported by researcher Lachlan Davidson, it let an unauthenticated attacker run arbitrary code on the server with a single HTTP request. A public proof of concept appeared the very next day. On December 5, CISA added it to its catalog of actively exploited vulnerabilities (KEV). The nickname stuck: React2Shell.
Eight months on, the flaw remains a textbook case, because it hits the core of React's modern architecture, and because the question it raises goes beyond this specific bug: what happens when a framework makes the boundary between client and server nearly invisible to the developer?
What the Flight protocol is
React Server Components (RSC) runs part of the component tree on the server, then sends the result to the browser. That result isn't HTML, but a stream serialized in a React-specific format called "Flight". It describes the rendered tree, references to the client components to load, promises still being resolved, and references to other chunks of the stream. It's a rich format: it can represent nested objects, cross-references between chunks, and values that arrive later.
The protocol also runs the other way. Server Functions (functions marked "use server", which Next.js calls Server Actions) let the browser call a server function as if it were local. On the wire, that's a POST request to the page, with a header identifying the action and, in the body, the arguments encoded... in Flight format. So the server has to deserialize a Flight stream supplied by the client. That's exactly where the flaw was.
How the flaw works
The analyses published by Rapid7, Qualys, Unit 42 and Akamai describe the same mechanism, with no need to go into the detail of a payload. The Flight deserializer resolves references inside the stream: one chunk can point to another, or to a property of another. In the vulnerable versions, that resolution didn't restrict the allowed paths tightly enough: a hand-crafted stream could walk up the JavaScript prototype chain of the objects involved, all the way to the Function constructor, the equivalent of an eval. From there, the server ran code supplied by the request.
It's a modern variant of a well-known family of flaws: unsafe deserialization. Java with its serialization gadgets, PHP with unserialize(), Python with pickle: each time, the problem is the same. A serialization format expressive enough to rebuild complex objects becomes a language, and if the input comes from a client, the client gets to write the program.
Vulnerable by default
The most important detail is also the most counterintuitive. React's advisory says so explicitly: an application could be vulnerable even if it declared no Server Functions at all, as long as it used React Server Components. The deserialization entry point existed as soon as the framework could handle actions. In other words, a Next.js App Router site without a single "use server" line was exposed. Many teams thought they were safe for exactly that reason.
CVSS score of CVE-2025-55182, the maximum possible, exploitable with no authentication or user interaction
Who was affected, exactly
The flaw lives in the packages implementing the server side of RSC: react-server-dom-webpack, react-server-dom-parcel and react-server-dom-turbopack, versions 19.0, 19.1.0, 19.1.1 and 19.2.0. It's fixed in 19.0.1, 19.1.2 and 19.2.1. Every framework bundling those packages inherits the problem: Next.js, React Router, Waku, RedwoodSDK, and the RSC integrations of Parcel and Vite.
For Next.js, the impact is tracked as CVE-2025-66478. Affected: App Router applications on Next.js 15.x and 16.x, plus canaries 14.3.0-canary.77 and later. Next.js 13.x, stable Next.js 14.x, Pages Router applications and the Edge runtime are not affected. The fixed versions, one per release line:
15.0.x -> 15.0.5 15.3.x -> 15.3.6 15.1.x -> 15.1.9 15.4.x -> 15.4.8 15.2.x -> 15.2.6 15.5.x -> 15.5.7 16.0.x -> 16.0.7 canary: 15.6.0-canary.58 / 16.1.0-canary.12 14.3.0-canary.77+ : downgrade to the latest 14.x stable
Hours, not days
The window between publication and exploitation was close to zero. Telemetry from several vendors shows automated scanning within hours of the announcement. Google Threat Intelligence Group observed exploitation by many groups, from opportunistic cybercriminals to suspected espionage clusters, deploying backdoors, network tunnelers and cryptocurrency miners. AWS documented rapid exploitation by China-nexus groups, and Unit 42 attempts aimed at containers and Kubernetes clusters across the major clouds.
Hosting and CDN providers (Vercel, Netlify, Cloudflare, Fastly, Akamai) rolled out filtering rules to block known malicious requests. But Vercel was clear: there is no workaround, only upgrading fixes it. A WAF rule blocks the variants it knows about; it doesn't repair the deserializer.
A week later, two more flaws
As often happens after a major flaw, attention turned to the same code. On December 11, 2025, React published two new vulnerabilities in the same deserializer. CVE-2025-55184 (CVSS 7.5) enabled denial of service: a self-referencing structure sent the server into a loop, process alive but no longer answering. CVE-2025-55183 (CVSS 5.3) could make the server return a Server Function's source code, including, where present, secrets hardcoded in it. An incomplete fix for the denial of service then led to CVE-2025-67779. None allows code execution, but all require moving to a version newer than the initial fix.
What to check today
The first check takes ten seconds: look at the versions actually installed, transitive ones included. A project can declare next@15 in its package.json and run, through its lockfile, on a version older than the fix.
npm ls next react react-dom --all npm ls react-server-dom-webpack react-server-dom-turbopack --all # Vercel's interactive checker / updater npx fix-react2shell-next
The instruction many ignored
Next.js recommends rotating every secret of any application that stayed online unpatched after December 4, 2025 at 1 PM Pacific. Upgrading closes the door; it doesn't invalidate keys an attacker may have read while it was open.
- Upgrade to the latest version in your release line, not just the first fix: the December 11 CVEs require a later version.
- Never hardcode a secret in a Server Function: CVE-2025-55183 showed their source code can leak.
- Monitor processes spawned by the Node.js server: a web server launching a shell or an unknown binary is the most reliable sign of exploitation.
- Restrict outbound network from the application container: a backdoor that can't reach its command server is far less useful.
For apps detected as Next.js, detektd includes a dedicated React2Shell check. It aims to establish whether the app exposes the vulnerable entry point on an unpatched version, without ever sending a payload able to execute anything, including on routes found while crawling, not just the homepage, since on many deployments the root is a static page.

29 million secrets on GitHub: what AI changes about leaked keys
GitGuardian's annual report counts 28.65 million secrets exposed on public GitHub in 2025, +81% for AI keys, and Claude Code-assisted commits leaking at twice the average rate. How a key ends up in a bundle, and how to stop it.

ChainDrop: an npm worm shipped 444 poisoned packages with valid signatures
On August 4, a self-propagating worm infected more than 400 npm packages downloaded around 2 billion times a month, through their maintainer's own CI pipeline. The signatures were valid. That's exactly the problem.