Understanding React2Shell (CVE-2025–55182)
The Critical RCE in React Server Components
Understanding React2Shell (CVE-2025–55182)
The Critical RCE in React Server Components
In December 2025, the web development community was rocked by the discovery of a critical vulnerability affecting the modern React ecosystem, specifically applications utilizing React Server Components (RSC) and frameworks like Next.js. Dubbed React2Shell and officially tracked as CVE-2025–55182, this flaw carries a maximum CVSS severity score of 10.0.
React2Shell is particularly dangerous because it allows an unauthenticated, remote attacker to execute arbitrary code on the server hosting the application with a single malicious HTTP request.
⚠️ DISCLOSURE & ETHICAL WARNING: This information is for educational and security awareness purposes only. The vulnerability has been patched. Do not attempt to use this information against any system without explicit authorization.

Gemini-generated image
I first came across this React2Shell vulnerability in this TryHackMe room. There is also a walkthrough room here, but I had a hard time trying to figure out and understand what exactly was happening. So I’ve done a deeper dive into what it is about and what the code is doing. In this article, we will break down the foundational concepts required to understand this flaw, explore the mechanics of the vulnerability, and examine a Proof of Concept (PoC) and how it works.
1. Foundations of the Vulnerability
To grasp how React2Shell works, we must first understand three core pillars of modern React architecture: React, Server-Side Rendering (SSR), and the Flight Protocol.
1.1. React and the Shift to Server Components
React is traditionally known as a library for building user interfaces on the client-side (in the user’s browser). However, the introduction of React Server Components (RSC) fundamentally changed this model. RSCs allow developers to write components that are executed exclusively on the server.
This split model aims to improve performance by offloading data fetching and heavy logic to the backend, reducing the bundle size sent to the client. When a user requests a page, the server executes these Server Components first.
1.2. Server-Side Rendering (SSR)
Server-Side Rendering is a technique where the initial HTML content of a webpage is generated on the server rather than in the browser.
- Request: A user requests a URL.
- Server Execution: The server renders the React component tree into a static HTML string.
- Response: The server sends this fully formed HTML to the client.
- Hydration: The client receives the HTML, displays it immediately (faster perceived load), and then downloads JavaScript to “hydrate” the page, making it interactive.
In the context of RSC, the server is doing more than just generating HTML; it’s managing a complex state of components, references, and data that needs to be synchronized with the client.
1.3. The React Flight Protocol
This is the piece of the puzzle where the vulnerability lies. When using React Server Components, the client and server need a way to communicate the structure of the UI. They don’t just exchange raw HTML; they exchange a specialized description of the component tree.
React Flight is the internal serialization protocol used to transport this component data between the server and the client.
- Serialized UI: On the server, React serializes the component tree into a unique, streamable format (often seen via the
/_rscendpoint in Next.js). - Streaming and Chunks: Flight transmits data in incremental “chunks” to allow the client to start rendering the page before the entire response is received.
- Deserialization: When these chunks arrive at the client (or are sent from client to server during a Server Action invocation), React must deserialize them — parsing the data and reconstructing the intended objects or components in memory.
Q. What is Deserializaton?
Deserialization is the process of converting a flat sequence of data (e.g. a string) back into its original, functional form (e.g. an object) in a computer’s memory.
The Flight protocol uses a specific serialisation format with type markers. For example:
$@denotes a chunk reference$Bdenotes a Blob reference- References can include property paths using colon separation (e.g.,
$1:constructor:constructor)
Q. What is a chunk?
A Chunk is a single unit of the stream. Think of it as a “row” or a “fragment” of the UI. When a React Server Component (RSC) renders, it might have to wait for data (like a database query). To avoid blocking the whole page, React sends the UI in chunks as they become ready.
Q. What is a Blob?
In web development, a Blob (Binary Large Object) represents raw data that isn’t necessarily text — like images, PDFs, or typed arrays (Buffer data). In the Flight Protocol, Blobs are used when a Server Component needs to pass heavy binary data directly to a Client Component without converting it into a massive, inefficient text string.
2. Inside the React2Shell Vulnerability
The core of React2Shell is an Insecure Deserialization flaw within the React Flight Protocol’s deserializer (implemented in packages like react-server-dom-webpack).
2.1 The Root Cause: Lack of Validation
When the Flight protocol deserializes a payload from a user request, it implicitly trusts that the payload originated from a legitimate, trusted React client. In the vulnerable versions, the deserializer failed to properly validate the types and structures being described in the incoming payload.
An attacker can craft a malicious HTTP request containing a serialized Flight payload that describes dangerous, unexpected objects instead of benign component data. Because of this lack of validation, the server will blindly reconstruct these dangerous objects.
2.2 The Attack Vector: Prototype Pollution to RCE
The heart of React2Shell is a critical flaw in the requireModule function within the react-server-dom-webpack package. This function is the "gatekeeper" responsible for loading JavaScript modules on the server whenever the React Flight Protocol references a Client Component or a Server Action.
The vulnerability stems from the use of unsafe bracket notation:
function requireModule(metadata) {
var moduleExports = __webpack_require__(metadata[0]);
// ... additional logic ...
return moduleExports[metadata[2]]; // VULNERABLE LINE
}
In JavaScript, accessing a property with object[key] doesn't just look at the object itself; if the key isn't found, the engine traverses the prototype chain. An attacker can manipulate metadata[2] to reference internal properties that were never intended to be accessible, bypassing the "safe" exports of the module.
2.3 From Property Access to RCE
An attacker can exploit this by specifying a property path that was never intended to be exported. Every JavaScript function and object is linked to a constructor property, which ultimately points to the global Function constructor.
Q. What exactly is a Global Function Constructor?
In JavaScript, the Function constructor is a built-in object used to create new functions dynamically from strings. You can think of it as a specialized version of
eval(). When you callnew Function('a', 'b', 'return a + b'), the JavaScript engine takes those strings and compiles them into a live, executable function in memory.
By using the Flight protocol’s colon-separated reference syntax (e.g., $1:constructor:constructor), an attacker can:
- Get Chunk 1: Access the legitimate module exports.
- Traverse the Prototype: Access the
.constructorproperty to obtain a reference to the global Function constructor. - Execute Code: Because the Function constructor can turn a string into executable code (similar to
eval()), the attacker can force the server to execute arbitrary commands.
2.3 Triggering the Execution
The requireModule function is triggered automatically whenever the Flight parser encounters a Module Reference (marked by $L or $@) in the incoming stream. As React attempts to "revive" this reference into a usable component, it passes the attacker's malicious path into the metadata array.
3. Proof of Concept (PoC) Exploit
Let’s look at a POC exploit from this link. Below is a key section of the exploit which we will examine:

Extract of POC code
3.1 The Trojan Horse (Fake Chunk)
In a normal Flight stream, a “Chunk” is a piece of data managed by the React engine. The attacker sends an object that looks like a Chunk but is manually filled with “poisoned” values.
**status: "resolved_model"**: This tells React, "I'm already finished loading; you don't need to wait for me, just process me."- The
thenproperty: This is the clever part. In JavaScript, if an object has athenmethod, it’s treated as a "Promise." By settingthento$1:__proto__:then, the attacker ensures that when React tries to "unwrap" the data, it triggers a specific sequence of internal functions.
Q. What is a Promise in Javascript?
A Promise is an object representing the eventual completion (or failure) of an asynchronous operation and its resulting value. It has the following components:
State: A status tracker — whether it has Pending, Fulfilled or Rejected.
Result: The eventual ‘output’ or return value. When a Promise is Pending, the result is
undefined. When it moves to Fulfilled, the result becomes the value passed intoresolve(value). When it moves to Rejected, the result becomes the error passed intoreject(error)
The Executor Function: The function where the logic lives.
The Handler Methods: .then(), .catch() and .finally(). These are like event listeners that gets triggered based on the State. In particular, .then() is called when State changes to Fulfilled.
3.2 Redirecting the “Getter” (The elegant swap)
This is the core of the trick. The attacker is exploiting the $B (Blob) handler. Normally, when React sees $B1337, it thinks: "I need to find a binary file with ID 1337. I'll look inside my internal storage (_formData) to find it."
The exploit swaps the “storage” with the “engine”:
- The Target: React’s internal code calls
_response._formData.get(_response._prefix + id). - The Swap: The attacker set
_formData.getto$1:constructor:constructor._formData.getis no longer a search function; it is now the Global Function Constructor. - The Arguments: React thinks it is passing a “File Name” to the
.get()function. Instead, it passesresponse._prefix(the malicious code) plus theid(1337).
3.3 The Execution (The Final Sink)
Now, the code execution happens automatically. Because the .get() function was replaced with the Function constructor, the line of code becomes

- How it runs: In JavaScript, calling
Function("code")creates a new function with that code inside. Because this happens during the "revival" phase of the Flight Protocol, React executes it immediately to try and "resolve" what it thinks is a Blob. - The command
execSync('xcalc')tells the operating system to open the calculator. In a real-world attack, this would be a command to download a virus or send the server's passwords to the attacker.
4. Putting the Exploit together
Now we look at the entire HTTP request to put things together. Again we are referring to the POC here in this link.

4.1 The Trigger: Next-Action: x
When the server sees this header, it stops treating the request as a normal page load. It says, “The user is trying to run a Server Action (like a form submission).” This forces the server to use the Flight Protocol Deserializer on the body of the request to understand what the user is sending.
Q. Why is Next Action ‘x’?
In a non-malicious request, Next-Action is a hash identifier that is assigned to a particular function on the server code that you want to execute. The form data provides the arguments to the function.
But in a React2Shell exploit, we don’t care if a real function exist. Why? Because the vulnerability happens the moment the server starts “reading” the form data to prepare the arguments. Before the server even realizes function ‘x’ doesn’t exist, it has already started parsing Field 0 and Field 1 of your form data to trigger the exploit
4.2 Field 0: The “Poisoned” Payload
This is the “Fake Chunk” we discussed earlier. When the server first encounters this, it just treats it as data and stores it in memory.
4.3 Field 1: The “Self-Reference” ($@0)
This is the most “elegant” part of the exploit.
**$@0is a Promise Reference. It tells the server: "The data for this part of the request is actually located back in Field 0.**"- The Loop: By pointing Field 1 back to Field 0, the attacker creates a circular reference. When React tries to “resolve” Field 1, it looks at Field 0. Because Field 0 has a
thenproperty, React treats Field 0 as a Promise.
4.4 Field 2: The “Closer” ([])
This is simply a structural requirement. React expects an array of arguments for the Server Action. By sending an empty array, the attacker satisfies the protocol requirements so the server doesn’t throw a “Missing Data” error before the exploit can run.
4.5 The Execution Flow
- RSC Trigger: The parser sees “Next-Action: x” so RSC is ‘triggered’ to parse the multipart form (without checking the function x).
- Deserialization: The server reads Field 0 and stores the malicious
_responseobject in memory. - Resolution: The server reads Field 1 (
$@0). It sees the reference and goes back to look at Field 0. - Method Hooking: Because Field 0 looks like a Promise (it has
then,status, andvalue), React's internal "Task" manager tries to "unwrap" it. Since the status is “resolved_model”, it automatically tries to execute then(), which points to $1.proto.then, which is essentially Chunk.prototype.then(). - The “Context” execution: The real
Chunk.prototype.thenruns using Field 0 asthis. Since status is “resolved”, it tries to ‘revive’ thevalue. - The Blob Trigger: Inside the
valueof Field 0, the attacker placed"$B1337". - The Sink: To “resolve” that Blob, React calls its internal Blob handler
_response._formData.get(_response._prefix + id). - The Explosion: Per section 3.2, we have already hijacked the Blob handler, replacing it with the Function constructor.
When to Hunt for React2Shell Vulnerability in a CTF
When doing a CTF, we can look for specific “environmental “ flags that suggest the server is vulnerable to Insecure Deserialization via the Flight Protocol.
1. The Technology Stack Fingerprint
The first sign is the presence of React Server Components (RSC). Watch for:
- Headers: Requests containing
Next-ActionorRSC: 1. - Frameworks: The application is built with Next.js (version 15.x or early 16.x) or a recent version of Waku.
- Network Traffic: When you interact with the site, you see
POSTrequests to the root/with weird, encoded text responses starting with1:I,2:H, or3:E.
2. The “Multipart” Clue
If you see a Server Action (a form submission) that uses Content-Type: multipart/form-data, pay close attention. CTF authors often use multipart forms for this exploit because they allow for a very clear separation between the "Fake Chunk" (Field 0) and the "Trigger" (Field 1).
3. Versions and Packages
If you manage to get file-system access (e.g., via a Local File Inclusion or by finding a package.json), look for the following packages:

4. The “Missing Action” Behavior
A major hint in a CTF is when a POST request to a Next-Action endpoint doesn't seem to "do" anything visible but takes a long time to respond.
5. Testing with a non-malicious POC request
If you can trigger a time-based command like execSync('sleep 10') and the response takes exactly 10 seconds longer, you have confirmed Remote Code Execution.
References
https://gist.github.com/maple3142/48bc9393f45e068cf8c90ab865c0f5f3#file-cve-2025-55182-http
메타데이터
- post_id
- d0eda51ee4dd
- slug
- understanding-react2shell-cve-2025-55182-d0eda51ee4dd
- url
- https://medium.com/@indigoshadowwashere/understanding-react2shell-cve-2025-55182-d0eda51ee4dd
- canonical_url
- https://medium.com/@indigoshadowwashere/understanding-react2shell-cve-2025-55182-d0eda51ee4dd
- author_url
- https://medium.com/@indigoshadowwashere
- status
- ok
- fetched_at
- 2026-07-11 17:08:33