React2Shell: Anatomy of a CVSS 10.0 Vulnerability

Table of Contents
- Introduction
- The Flight Protocol: A Double-Edged Sword
- The Exploit Chain: From JSON to Shell
- The Fix: One Line of Defense
- Remediation
- Key Takeaways
- References
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:
_formData.getbecomes$1:constructor:constructor, which resolves to the Function constructor_prefix + idbecomes 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
-
Deserialization is dangerous: Any protocol that reconstructs objects from untrusted input is a potential RCE vector. Flight's complexity made it especially vulnerable.
-
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.
-
One line can make the difference: The fix was remarkably simple—a
hasOwnPropertycheck. The lesson: defensive coding patterns matter at every level. -
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.
-
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.



