The React2Shell Story – From Curious Protocol to Remote Code Execution
#Vulnerabilities

The React2Shell Story – From Curious Protocol to Remote Code Execution

AI & ML Reporter
6 min read

Lachlan Davidson recounts how a deep dive into React’s undocumented “Flight” protocol uncovered a critical remote‑code‑execution flaw (CVE‑2025‑55182) that affected millions of Next.js sites, explains the technical chain that made the exploit possible, and outlines the practical limits of the vulnerability after Meta’s patch.

The React2Shell Story – From Curious Protocol to Remote Code Execution

Published 30 Nov 2025 – updated 3 Dec 2025
Author: Lachlan Davidson


What was claimed?

On 3 Dec 2025 Meta released a security advisory (CVE‑2025‑55182) describing a remote code execution (RCE) vulnerability in the React Server Components/Server Functions stack – popularly known as React2Shell. The advisory urged every Next.js site to upgrade immediately, implying that an attacker could gain arbitrary shell access on any server running a vulnerable version of React.

What is actually new?

The core of the issue is not a bug in user‑code but a flaw in React’s Flight protocol, the low‑level serialization format that lets a browser send rich JavaScript values (Dates, Maps, Promises, circular references, etc.) to the server. The protocol was deliberately kept undocumented, and its implementation makes a few dangerous assumptions:

  1. Prototype‑chain traversal is allowed. Flight lets a payload reference any property on an object, including inherited ones. This means a client can request Number.prototype.toString or Array.prototype.push and have the server place the resulting function on an attacker‑controlled object.
  2. Thenable objects are eagerly awaited. The Flight decoder calls await decodeReply(...). Under the hood this invokes Promise.resolve, which treats any object with a .then method as a thenable and automatically calls it. By sending a crafted object whose .then points at a built‑in prototype method, the attacker can execute arbitrary JavaScript on the server.
  3. Chunk objects inherit from Promise. The protocol uses a custom Chunk class (a Promise subclass) to represent asynchronous pieces of the payload. By using the $@x syntax the client can create a promise‑like chunk that the server treats as a real Chunk. When the server later accesses Chunk.prototype.then on this attacker‑controlled instance, it ends up invoking React’s internal then implementation with attacker‑supplied state.
  4. Server‑manifest indirection. Server Functions are looked up via a manifest that maps an ID to a module export. The exploit chain eventually tricks React into loading the built‑in Node module module and calling its internal _load function, which can evaluate arbitrary code.

The final payload is a series of Flight chunks that:

  • Create a fake Chunk whose internal status and reason fields are under attacker control.
  • Use $@ to make the server treat a reference as a pending chunk.
  • Supply a .then that points at Chunk.prototype.then, causing React to invoke the internal promise handling on the attacker object.
  • Hijack the manifest lookup to reach module._load and execute Function("…evil…")().

When the proof‑of‑concept script is run against a fresh Next.js 14 app (React 18.3+), the server spawns a shell and prints the attacker’s message – a classic RCE demonstration.

Limitations and mitigations

While the advisory is serious, the attack surface is narrower than the headline suggests:

Limitation Reason
Requires Server‑Side Rendering (SSR) with Flight Pure client‑only React apps or static‑site generation (SSG) do not deserialize Flight payloads, so they are unaffected.
Only works on Node.js runtimes The exploit relies on Node’s module system (module._load). Deployments that run React on Deno, Bun, or edge runtimes that bundle away module are not vulnerable.
Manifest must be reachable If a project disables Server Functions or uses a custom manifest that does not expose arbitrary modules, the chain cannot reach module._load.
Chunk IDs must be predictable The payload depends on the internal chunk numbering scheme used by the build. Production builds that enable deterministic chunk IDs (via output.chunkFilename in Webpack/Turbopack) make exploitation harder, though not impossible.
Patch is already released Meta’s fix removes the $@ shortcut and adds strict prototype checks inside the Flight decoder, preventing arbitrary prototype access and thenable execution.

In practice, the vulnerability is exploitable only against applications that:

  1. Run a vulnerable version of React (≤ 18.3.0) with Flight enabled (i.e., use Server Actions/Server Functions).
  2. Deploy on a Node.js server that bundles the standard module loader.
  3. Accept unvalidated Flight payloads from the client – which is the default behaviour for any public Next.js endpoint that exports a Server Function.

How the discovery happened

Lachlan’s post reads like a forensic diary:

  • Monday – curiosity about the undocumented “Flight” format leads to a manual reconstruction of its chunk syntax.
  • Tuesday – experiments show that referencing prototype properties works; a simple toString injection is demonstrated.
  • Wednesday‑Thursday – the team discovers that any object with a .then method becomes a thenable that await will call, and that $@ creates a Promise‑like chunk.
  • Friday – by chaining thenables and spoofing a Chunk’s internal state, they reach the internal Chunk.prototype.then implementation, which eventually calls the server manifest.
  • Saturday – after several false starts with the manifest, the exploit lands on module._load, allowing arbitrary JavaScript execution.
  • Sunday – a minimal PoC is submitted; Meta reproduces it within 17 hours and ships a patch.

The story illustrates a classic security research pattern: deep protocol understanding → small unchecked assumption → chain of native primitives → RCE.

Aftermath and practical advice

  • Upgrade immediately. The patched versions are available in the Next.js release notes and the React 18.3.1 patch.
  • Audit Server Functions. Ensure that any parameter expected to be a primitive string is validated at runtime (e.g., if (typeof name !== "string") throw new TypeError();).
  • Consider disabling Flight. If you do not need Server Actions, turn them off via experimental.serverActions: false in next.config.js.
  • Run on non‑Node runtimes where possible. Edge‑runtime deployments (Vercel Edge Functions, Cloudflare Workers) are not affected because they lack the Node module loader.
  • Monitor for abuse. Sylvie Mayer’s follow‑up blog post (linked below) contains a scanner that looks for the specific Flight patterns used in the exploit; running it against your logs can help spot attempted attacks.

Further reading

The React2Shell Story | Lachlan Davidson | Blog

The diagram shows how a Flight chunk ($@2) creates a fake Chunk that later triggers Chunk.prototype.then, leading to the module loader.


Bottom line: The React2Shell bug was not a “magical” new attack surface; it was the result of a series of reasonable‑looking design choices (prototype traversal, thenable handling, promise‑subclassing) that, when combined, gave an attacker a path to Node’s module system. The fix is straightforward – tighten prototype checks and reject untrusted thenables – but the episode is a reminder that even battle‑tested frameworks can hide dangerous corners when they expose low‑level serialization protocols to the internet.

Comments

Loading comments...