Introduction

React2Shell is a vulnerability in React Server Components which gives an attacker unauthenticated remote code execution on a remote server. This vulnerability has CVSS score of 10.0 💀. If you are curious like me and wants to know what happens on the backend code which cause this remote code execution this blog is for you. Before diving in to code let’s start with some basics.

What are React Server Components?

React Server Components (RSC) are a feature in React which is used in React frameworks like Next.js. They are used to segregate client-side and server-side rendered components. Static or non-interactive part of the website is rendered on the server side and sent it to the user in the HTTP response. Interactive elements like button or links are only rendered on client side. It is used to make website respond faster for users.

Without React Server Components(RSC) :

  • Server would send full HTML OR JSON
  • Client would re-render everything
  • More JS is executed on the client side
  • Heavy JS is downloaded on the client side browser

With React Server Components(RSC) :

  • Server sends client component structure
  • Client merges it with existing UI
  • Only interactive JS is executed on the client side
  • Decreases the size of JS bundle on the client side browser

React uses flight protocol to send and receive RSC data from server to the client.

Flight Protocol

Flight protocol is a special protocol used by React to send React Server Components data from the server to the client or browser. It has a JSON like format but not exactly JSON.

Server side rendered HTML is sent from the server to the client browser and the hydration process starts at the client side using react runtime to make the UI interactive.

Hydration is the process of attaching client-side JavaScript logic with the static HTML that was initially rendered on the server.

Structure of flight payload :

1:I["app/components/Counter.js","default"]
2:["$","div",null,{
  "children":[
    ["$","h1",null,"Dashboard"],
    ["$","$L1"]
  ]
}]

How React Server Components works?

Let’s start with an example -

app/
 ├── page.js (Server Component)
 ├── components/
 │     ├── Posts.js (Server Component)
 │     └── Like.js (Client Component)

‘page.js’ and ‘Posts.js’ is executed on the server side and generates a HTML shell.

Server creates below RSC payload and send it to the client.

1:I["app/components/Like.js","default"]
2:["$","div",null,{
  "children":[
    ["$","h3",null,"Post title"],
    ["$","$L1"]
  ]
}]
  • I → import client component
  • $L1 → reference to Like.js client component

Server Rendered HTML shell and RSC payload is received on the client browser from the server.

Browser parsed the RSC payload using React runtime and make the HTML interactive and ready to use.

Lab Setup and debugging

I have used below command to setup a react2shell lab on my local computer. Recommended to use it in an isolated network or in VM through which it is only accessible locally.

npm create next-app@16.0.6 react2shell

Start the server and attach the debugger using below command.

set NODE_OPTIONS=--inspect && npm run dev

Attach Chrome: Open chrome://inspect in your browser and click Open dedicated DevTools for Node.

Note - Even though we are using chrome browser to debug the code and set the breakpoints, remeber that it is a server side code we are debugging.

I have crafted below HTTP request from my BurpSuite to send payload to the react server.

Payload

We are sending two chunks in the request :- ‘0’ and ‘1’.

Payload Chunk :-

  0: {
    status: "resolved_model",
    reason: 0,
    then: "$1:then",
    value: "{"then":"$B1337"}",
    _response: {
      _prefix: "var res=process.mainModule.require('child_process').execSync('whoami').toString('base64');throw Object.assign(new Error('x'),{digest: res});",
      _formData: {
        get: "$1:then:constructor"
      }
    }
  },
  1: "$@0",
}

“$@0” means chunk ‘1’ is pointing to chunk ‘0’.

The vulnerability lies in the react-server-dom-webpack, react-server-dom-parcel, and react-server-dom-turbopack packages of React.

I have set some breakpoints in the react-server-dom-webpack package which we will inspect and see how our payload get processed on the server.

When I send http request from my burpsuite with the payload mentioned above it gets sent to this function ‘initializeModelChunk’.

React2Shell

‘initializeModelChunk’ function is called for every chunk we send in the request.

Image description

‘rawModel’ is the JSON parsed version of our payload we passed.

Then it passes the parsed JSON chunk to ‘reviveModel’ function.

Image description

‘reviveModel’ function reconstructs the chunk and checks key and value of each of property in the chunk one by one.

It checks if the value is string and then pass the value to ‘parseModelString’ function.

Image description

‘parseModelString’ function checks if the value starts with ‘$’ and then it checks the value after ‘$’ symbol which is a chunk number, in our case it is ‘1’. It waits for the chunk ‘1’ to be parsed and keep the value as null till the chunk is parsed by React.

Image description

If the value does not start with ‘$’ then it simply returns its value.

Image description

As mentioned above, it sets the vlaue of ’then’ and ‘get’ to null and is waiting for the chunk ‘1’.

Image description

React should not allow internal objects like ’then’, ‘value’, ‘_response’, ‘proto’ or ‘prototype’ to be used from the user input. This is where the vulnerability creeps in.

In JavaScript, there is a concept called ‘duck typing’. It means if an object ‘quacks like a duck’ or ‘walks like a duck’ then it is a duck🦆. So, it means if the object has ’then’ property then it is a ‘Promise’.

A Promise in JavaScript is an object that represents the result of an asynchronous operation i.e something that will complete in the future not immediately.

Image description

Now, ‘initializeModelChunk’ function is called for the chunk ‘1’.

Chunk ‘1’ is parsed wich has ‘$@0’ value. It means it is self referencing to chunk ‘0’.

Image description
Image description

‘wakeChunk’ function invoke ‘resolveListeners’ which are ‘$1’ value in the chunk ‘0’ and are waiting for chunk ‘1’ to initialize so that they can access chunk ‘1’ properties.

As mentioned above, it waits for the chunk ‘1’ to be parsed and keep the value as null till the chunk is parsed by React. So the vlaue of ’then’ and ‘get’ was set to null and was waiting for the chunk ‘1’.

then: "$1:then" from chunk ‘0’ is invoked and it’s value is accessed from chunk ‘1’. It means go the chunk ‘1’ and access its ’then’ property.

Image description

then in the parentObject i.e in chunk ‘0’ has became a function now which was set to null initially.

Image description

get: "$1:then:constructor" from chunk ‘0’ is invoked and it’s value is accessed from chunk ‘1’. It means go the chunk ‘1’ and access its ’then’ property which becomes a function and then access .then function’s ‘constructor’ property which gives us eval like function.

This is what causes remote code execution.

"$1"              → resolve chunk 1
"$1:then"         → resolve chunk 1, then access .then
"$1:then:constructor" → resolve chunk 1, access .then, access .constructor
Image description

get in the parentObject i.e in chunk ‘0’ has became a eval like function now which was set to null initially.

Image description

then property makes the object thenable and then it checks if the status of the chunk is resolved_model and that is why we passsed then: "$1:then" and the status value of the chunk ‘0’ as resolved_model otherwise our payload will not work.

Now, ‘initializeModelChunk’ function is called again but this time the chunk it is parsing is controlled by us based on the values we passed.

Image description

value: "{"then":"$B1337"}" is parsed and then due to then property React will treat it as thenable.

Image description

B from $B1337 has a special case in which it calls the response._formData.get which we have seen earlier has become eval like function and then it passess prefix value to it from the _response.

Image description

React treats then as a promise and in JavaScript then chains are automatically executed by the runtime which causes our code to get executed.

Image description
Image description

We send the malicious RSC payload via Burp Suite and receive a 500 Internal Server Error in the response. However, the response body contains a digest property whose value is the base64-encoded output of the command executed on the server confirming successful Remote Code Execution.

Image description

Remediation

React shouldn’t allow user input to include this internal React properties like status, then, _response,etc.

React fixed this vulnerability using hasOwnProperty, which checks if a property belongs directly to an object or is inherited from its prototype chain. Without this check, the RSC reference resolver blindly walked up the prototype chain allowing attackers to reach Function via $1:then:constructor, where .constructor is not an own property of .then but an inherited one from Function.prototype.

Patched Versions:

  • React: 19.0.1, 19.1.2, 19.2.1

  • Next.js: 5.0.5, 15.1.9, 15.2.6, 15.3.6, 15.4.8, 15.5.7, 16.0.7

References