Go back

React2Shell: how the React Flight protocol became an RCE attack vector 🚨

React2Shell and CVE-2025-55182 explained: React Flight deserialization, the exploit chain and defensive measures.

React2Shell: how the React Flight protocol became an RCE attack vector 🚨

The move to React Server Components (RSC) and Server Actions was presented as the ecosystem’s biggest architectural update in years. But blurring the boundary between client and server brought fundamental security risks with it.

Smashing Magazine has published a detailed analysis of CVE-2025-55182 (known as React2Shell), which received the maximum severity score: CVSS 10.0.

At the heart of the problem is the text-based React Flight protocol, used to stream data and reconstruct the UI tree from the server on the client.

🧠 What is the vulnerability in the Flight protocol?

Unlike plain JSON, the newline-delimited Flight protocol transmits references to executable code, RPC endpoints and client modules. Put simply, every Flight stream is a deserialization boundary.

The root cause of the critical RCE turned out to be mundane: the getOutlinedModel function in React’s internal packages lacked a hasOwnProperty check. This enabled an attack chain through prototype chain traversal.

⚡️ The anatomy of the exploit in 5 steps:

  1. Walking the prototype chain: Using Flight’s prefix syntax ($:), an attacker moves from a base object to Object.prototype ➔ Object ➔ and reaches the built-in Function constructor (a native equivalent of eval()).
  2. Intercepting internal state: The $@0 reference returns a raw Chunk instead of a resolved value, exposing mutable access to the internals of React’s pipeline.
  3. Thenable hijack: Overwriting a chunk’s .then makes the React runtime think it is a Promise and invoke await.
  4. Replacing the context: On a second deserialization pass, _response._formData.get is redirected to the captured Function, while the shell command is hidden in _response._prefix.
  5. Triggering execution: Invoking the $B0 blob executes Function("attacker_command")(). The result is code running directly on the server with the Node.js process’s privileges.

🛡 What should you do, and how can you defend against it?

The problem demonstrated that treating a Flight stream as “internal framework magic” is no longer acceptable: it is untrusted external input.

  1. Patch versions right now: Upgrade react, react-dom and Next.js to recent releases with the corrected hasOwnProperty check.
  2. Validate Server Actions: Do not rely on TypeScript’s automatic typing at runtime. All Server Action arguments must be strictly validated with Zod or Valibot at the entry point.
  3. Isolate modules with server-only: Explicitly add import 'server-only' to all files containing business logic and secrets to keep them out of the Flight context.
  4. Configure a WAF: Filter incoming Server Action requests for anomalies such as $: and __proto__.

Read the full breakdown

Copied