React2Shell: Anatomy of a CVSS 10.0 Vulnerability

By Ryan Wentzel
5 Min. Read
#Incident Anatomy#Cybersecurity#Web Security#AppSec#Vulnerability Analysis
React2Shell: Anatomy of a CVSS 10.0 Vulnerability

Table of Contents

Introduction

In the world of web application security, it is rare to see a vulnerability that is simultaneously ubiquitous, critical (CVSS 10.0), and architecturally fascinating. React2Shell (CVE-2025-55182) is exactly that.

Disclosed on December 3, 2025, this unauthenticated Remote Code Execution (RCE) vulnerability affects the core serialization protocol of React Server Components (RSC). It allows attackers to execute arbitrary code on servers running default configurations of Next.js and other RSC-enabled frameworks.

CVE Product CVSS Score Impact
CVE-2025-55182 React (react-server-dom-*) 10.0 Remote Code Execution
CVE-2025-66478 Next.js 10.0 Remote Code Execution

This post dives into the technical details of the "Flight" protocol, the specific deserialization gadget chain used for exploitation, and the patch that killed the bug.

The Flight Protocol: A Double-Edged Sword

To understand React2Shell, you must understand Flight—the internal protocol React uses to stream component trees from server to client (and back via Server Actions).

Unlike REST or GraphQL, which typically traffic in JSON, Flight needs to serialize complex React-specific concepts:

  • Chunks ($@): References to lazy-loaded code or modules
  • Blobs ($B): Binary data streams
  • Promises ($): Asynchronous operations

When a client invokes a Server Action (e.g., submitting a form in Next.js), the arguments are serialized into a string that looks like this:

0:{"name":"$@1"}
1:{"id":"./src/actions.js","chunks":[],"name":"updateUser"}

The server parses this stream, resolving the references to execute the corresponding function. The vulnerability lies in the react-server-dom-webpack (and related) packages, where the parser failed to validate that the objects it was "rehydrating" actually belonged to the server's trusted internal state.

The Core Problem: Implicit Trust

The Flight parser was designed with an implicit trust model—it assumed that incoming data would always conform to expected structures. This assumption proved fatal when researchers discovered that carefully crafted payloads could manipulate the parser's internal state through prototype pollution.

The Exploit Chain: From JSON to Shell

The exploit, first weaponized by researchers like maple3142, is a masterclass in JavaScript runtime manipulation. It leverages Server-Side Prototype Pollution to trick the Flight parser into executing a "gadget chain."

The attack requires a single HTTP POST request with a multipart/form-data body (standard for Server Actions).

Stage 1: The Fake Chunk (Entry Point)

The attacker sends a JSON object that looks like a React Chunk but contains a malicious then property:

{
  "then": "$1:__proto__:then",
  "status": "resolved_model",
  "value": "{\"then\":\"$B1337\"}",
  "_response": {...}
}

The Trigger: The Flight parser treats any object with a then property as a Promise (a "thenable"). It attempts to await it.

The Trick: The attacker sets then to $1:__proto__:then. In the Flight syntax, this resolves to Chunk.prototype.then.

The Result: The server executes Chunk.prototype.then.call(fakeChunk). This forces the runtime to re-enter the parsing loop using the attacker's fake object as the context (this).

Stage 2: The _formData Gadget

Once the parser is forced to process the malicious chunk, it attempts to resolve the value provided in the payload: "$B1337" (a Blob reference).

To resolve a Blob, the React internal code accesses a specific property on the response object: _formData. Under normal execution, this is a trusted FormData object. In the exploit, the attacker has polluted this (the fake chunk) with their own _response object.

The vulnerable code looks something like this:

response._formData.get(response._prefix + id)

Stage 3: Execution (The Sink)

The attacker controls the _response object to pivot execution to the global Function constructor:

"_response": {
  "_prefix": "process.mainModule.require('child_process').execSync('xcalc');",
  "_formData": {
    "get": "$1:constructor:constructor"
  }
}

Here is the substitution that happens at runtime:

  1. _formData.get becomes $1:constructor:constructor, which resolves to the Function constructor
  2. _prefix + id becomes the attacker's payload string (the code to execute)

The call _formData.get(prefix + id) effectively becomes:

new Function("process.mainModule.require('child_process').execSync('xcalc');...")

Because this occurs inside a Promise resolution chain, the created function is immediately invoked, executing the shell command on the server.

Attack Flow Visualization

Stage Action Result
1 Send fake chunk with then property Parser treats object as Promise
2 then resolves to Chunk.prototype.then Context hijacked to attacker's object
3 Blob resolution triggers _formData.get() Attacker controls method and arguments
4 Function constructor invoked with payload Arbitrary code execution

The Fix: One Line of Defense

The fix, merged in facebook/react#35277, introduces a strict check during module resolution. The developers replaced a direct property access with hasOwnProperty.call.

Before (Vulnerable):

return moduleExports[metadata[NAME]];

After (Patched):

if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
  return moduleExports[metadata[NAME]];
}

This prevents the parser from traversing up the prototype chain (__proto__) to access inherited methods like constructor or then, effectively neutralizing the gadget chain.

Why This Works

The hasOwnProperty.call() pattern ensures that only properties directly defined on the object (not inherited from the prototype chain) are accessed. This is a fundamental defensive pattern against prototype pollution attacks:

  • Direct access: obj[key] → Can traverse prototype chain ❌
  • Safe access: hasOwnProperty.call(obj, key) → Only own properties ✅

Remediation

If you are running React 19 or Next.js 15/16, you are likely vulnerable by default.

Update Immediately

Framework Patched Versions
Next.js 15.0.5, 15.1.9, 15.2.6, 15.3.6, 15.4.8, 15.5.7, 16.0.7 (or later)
React react-server-dom-webpack 19.0.1, 19.1.2, 19.2.1

Important Notes

  • No configuration workarounds exist. This is a code-level vulnerability in the framework's core.
  • Vercel-hosted applications benefit from platform-level WAF rules that block malicious request patterns, but upgrading is still mandatory.
  • React 18 applications are not affected (RSC is a React 19 feature).

Verification

After updating, verify your versions:

npm list next react-server-dom-webpack

Key Takeaways

  1. Deserialization is dangerous: Any protocol that reconstructs objects from untrusted input is a potential RCE vector. Flight's complexity made it especially vulnerable.

  2. Prototype pollution is not theoretical: This real-world exploit demonstrates how JavaScript's prototype chain can be weaponized for code execution, not just property manipulation.

  3. One line can make the difference: The fix was remarkably simple—a hasOwnProperty check. The lesson: defensive coding patterns matter at every level.

  4. Framework trust is implicit risk: When you adopt a framework, you inherit its attack surface. React Server Components introduced powerful capabilities but also new vectors.

  5. Patch velocity matters: From disclosure (December 3) to widespread awareness (December 7), the window for exploitation was narrow but real. Automated dependency updates are no longer optional.

The React2Shell vulnerability is a reminder that even the most widely-used, well-audited frameworks can harbor critical flaws. The sophistication of the exploit chain—leveraging thenables, prototype pollution, and the Function constructor in sequence—demonstrates the creativity of modern security research and the importance of defense-in-depth.

References

Share Your Thoughts

Found this article helpful? Share it with your network.

Get in Touch