R&D React2Shell series · 2 / 3

React2Shell Part 2: How RSC and Flight Deserialization Led to Server RCE

한국어로 읽기 →
SERIESReact2Shell Response3 parts in total
  1. 01 React2Shell Part 1: Vulnerability Overview and Response PrioritiesRead →
  2. 02 React2Shell Part 2: How RSC and Flight Deserialization Led to Server RCENow reading
  3. 03 React2Shell Part 3: After React2Shell, How Should Companies Rebuild Their Response Framework?Read →
Contents
  1. 11. Vulnerability Overview
  2. 22. RSC and Flight: The Architecture Behind the Problem
  3. 33. Prototype / Context Pollution, Blob Tokens, and Thenables
  4. 44. Three Mistakes in the Flight Deserialization Design
  5. 55. Summary: A Framework Is Not a “Trusted Black Box”
  6. 6🔜 Next (Part 3): “After React2Shell: How Should Companies Rebuild Their Response Framework?”

On December 3, 2025, a vulnerability enabling server-side remote code execution (RCE) in React Server Components (RSC) environments, React2Shell(CVE-2025-55182), was disclosed.

It is rated Critical with a CVSS score of 10 out of 10, and its EPSS (Exploit Prediction Scoring System) score was high at 77.80% as of December 9.

It has also been officially added to the KEV (U.S. CISA Known Exploited Vulnerabilities) list and is classified as a threat with a high likelihood of real-world exploitation.

The ZIEN Innovation Research Team took a technical interest in this CVE and conducted an in-depth analysis.

1. Vulnerability Overview

1.1 Basic Information

  • Vulnerability name: React2Shell

  • CVE Number: CVE-2025-55182 – a vulnerability in React Server Components (RSC) / Flight deserialization. CVE-2025-66478 was reported on the Next.js side but has been merged as a duplicate of the above CVE.

  • Severity: CVSS 10.0 (Critical, server-side RCE possible); EPSS 77.80% (as of 2025-12-11, very high likelihood of exploitation); added to KEV (2025-12-05) – officially classified as exploited in the wild

  • Reporting and disclosure: 2025-11-29: researcher Lachlan Davidson first reported it to Meta (Facebook); 2025-12-03: official patch and React2Shell website released

※ EPSS (Exploit likelihood): an ML-based prediction score provided by First.org based on vulnerability data
※ KEV (Known Exploited Vulnerabilities): a list maintained by U.S. CISA that officially identifies vulnerabilities exploited in the wild

1.2 Core Problem Structure

React2Shell arises from three design flaws operating together.

  1. Unsafe Deserialization: No validation of Flight reference strings

  2. Prototype / Context Pollution: RSC internal context objects can be polluted with user input

  3. Thenable-based automatic execution: under the thenable rule, then() is called automatically, triggering RCE

These three elements connect naturally on top of the new rendering/data model of RSC + Flight + Server Actions, ultimately leading to server-side code execution.

2. RSC and Flight: The Architecture Behind the Problem

2.1 The Evolution of Rendering Models

Web rendering has evolved as follows.

[Figure 1] Timeline of rendering models by era

  1. Traditional server rendering (PHP / JSP / Rails, etc.)

    • The server renders complete HTML and sends it

    • Every page transition triggers a full reload; state is hard to keep and real-time needs are limited

  2. jQuery + Ajax

    • Server templates + Ajax fetch only data and update parts of the DOM

    • Structural limits raise maintenance costs for large, complex UIs

  3. SPA / CSR (React / Angular / Vue)

    • The server sends empty HTML + a JS bundle; rendering, routing, and state management run in the browser

    • Interactivity improved, but initial load, SEO, and bundle-size problems persisted

  4. SSR + CSR hybrid (Next.js, etc.)

    • Initial HTML is rendered on the server (SSR), then hydration makes it behave like an SPA

    • It mitigates some CSR drawbacks, but hydration cost, data duplication, and waterfall issues remain

  5. SSG / JAMstack / ISR

    • HTML is pre-generated at build time (SSG) and served from a CDN

    • Good for static content, but limited for dynamic or per-user pages

  6. RSC(React Server Components)

    • Splits components into server and client components

    • Server components run only on the server and are not included in the JS bundle; their output is serialized with the Flight protocol and streamed to the client

RSC maximizes performance by splitting components into server components and client components, and streams the output of server-executed components in a dedicated serialization format called the Flight protocol.

React2Shell stems from a deserialization design flaw in this RSC + Flight layer.

2.2 RSC and the Flight Serialization Format

To handle structures that are hard to express in JSON (Promises, Blobs, Server Actions, etc.), RSC uses a dedicated serialization format called Flight.

Key characteristics of Flight:

  • It consists not of a single JSON blob but of multiple stream chunks

  • Each chunk consists of a unique ID and serialization information

  • The client interprets the stream by filling an id → value mapping table

  • Types and references are expressed with specific string tokens

Example tokens:

  • $D… : Date

  • $undefined, $null : special values

  • $@id : references the entire chunk with that ID (e.g., $@0 → the value of chunk id 0)

  • $Bhex : a token that indirectly references a Blob/file ($B1337, etc.)

  • "1:foo:bar" : a reference string meaning “follow the path .foo.bar from the value of the chunk with ID 1”

In other words, a Flight payload is closer to a mini-language describing “which path to follow from which chunk, and how”.

During deserialization, these tokens are trusted as-is to access internal JS objects, look up Blobs, and reference server functions.

The problem in React2Shell is that these strings were used in internal object-access flows without any validation.

2.3 How Server Actions Relate to Flight

In React 19 / Next.js App Router, Server Actions (Server Functions) work as follows.

  1. The client submits a form or calls await someAction(formData)

  2. The browser sends it to the server as multipart/form-data

    (including regular fields, files (Blobs), RSC metadata, Flight reference strings, etc.)

  3. The server handles it at an endpoint such as /app/_actions

    a. Parse the HTTP request → create FormData / body

    b. Call the Flight deserializer to restore it into “a model structure React understands”

    c. Find and execute the Server Function to be called within the model

The vulnerability occurs at step 3-b, when the Flight deserializer trusts attacker input as-is.

3. Prototype / Context Pollution, Blob Tokens, and Thenables

To understand React2Shell, it helps to look at the following three concepts separately.

  1. Prototype / Prototype Pollution

  2. Context Pollution (pollution of RSC internal context)

  3. Blob token handling and the thenable auto-invocation rule

3.1 Prototype and the constructor.constructor pattern

Every JavaScript object works on the basis of a prototype chain.

  • {} → Object.prototype

  • [] → Array.prototype

And the following chain access is possible.

({}).constructor             // Object
({}).constructor.constructor // Function
Function("console.log(1)")() // executes a string as code

In other words, constructor.constructor is a typical primitive for reaching the Function constructor, and when this value is connected to an actual call flow, it can lead to server-side code execution (RCE).

3.2 Prototype Pollution vs Context Pollution

  • Prototype Pollution: an attack that pollutes a shared prototype such as Object.prototype via __proto__ during JSON parsing/merging; once polluted, every object created afterwards is affected

  • Context Pollution: leaves the global prototype untouched and instead overwrites “the context object itself” used by a specific library or framework with attacker values

React2Shell used the following combination.

  • Reach __proto__.constructor.constructor through a Flight reference string → obtain Function

  • Use the obtained Function and inject it again into fields of the RSC internal context object (_formData.get, _prefix, then, etc.)

In other words, a primitive obtained via a typical Prototype Pollution path is inserted into React internals in the form of Context Pollution.

3.3 The Role of Blob Tokens ($B...)

In Flight, a Blob token is an indirect reference string to a Blob file.

(example)
$B1337
 - $B   : Blob token type
 - 1337 : Blob ID (hex)

The server processes it as follows.

(example)

function parseModelString(response, value) {
  // e.g., "$B1337"
  if (typeof value === "string" && value.startsWith("$B")) {
    return resolveBlobToken(response, value);
  }

  // other tokens ("$@", "1:foo:bar", etc.) are handled in other branches
  return value;
}

function resolveBlobToken(response, token) {
  const idHex = token.slice(2);   // "1337"
  const id = parseInt(idHex, 16); // 0x1337

  const key = response._prefix + id;    // build the actual key from prefix + id
  return response._formData.get(key);   // look up the Blob in FormData
}

The original intent is simple: during model revival, when a string matches the $B… pattern, parseModelString() → resolveBlobToken() is called, and each time it is designed to trigger a _formData.get(_prefix + id) call.

  • _prefix: internal prefix used to build Blob keys

  • _formData: actual data parsed from multipart/form-data

  • _formData.get(key): returns the Blob object

In React2Shell, however, all three of these elements could be placed under attacker control.

  • _prefix: originally intended as an “internal key prefix,” but an attacker can insert an arbitrary JS code fragment

  • _formData.get: originally a FormData method, but it can be replaced with the Function constructor via constructor.constructor

  • $Bxxxx token: a Blob reference token that in practice becomes the execution point (trigger) where the two values above are combined

3.4 Thenables and the Promise Resolution Rule

A thenable is “an object that is not a genuine Promise instance but has a then() method.”

const fake = {
  then(resolve, reject) {
    console.log("then called");
    resolve(42);
  }
};

Promise.resolve(fake);
// prints "then called" automatically

Under the standard rule (Promise Resolution Procedure), it works as follows.

  • Promise.resolve(x): when called, if x is a thenable, x.then(resolve, reject) is called automatically

  • await x also internally calls Promise.resolve(x)

RSC leverages this rule to manage Flight chunks like thenables.

Conceptually, each chunk looks like this:

const chunk = {
  status: "pending",
  value: rawModel,
  then(resolve, reject) {
    try {
      // restore the model into final JS objects
      const result = reviveModel(response, this.value);
      this.status = "resolved_model";
      resolve(result);
    } catch (e) {
      reject(e);
    }
  }
};

At certain points React processes such a chunk in a way similar to Promise.resolve(chunk), so under the standard rule, chunk.then(...) is called automatically.

And inside this then, reviveModel() is called, which recursively traverses strings/arrays/objects as shown earlier, and when a string matches the $Bxxxx pattern, it

processes the Blob token through the parseModelString() → resolveBlobToken() path defined in 3.3.

In other words, it is a structure in which “the thenable auto-execution point” and “the Blob token handling point” are bound together in the same execution flow.

4. Three Mistakes in the Flight Deserialization Design

React2Shell arises when the following three design flaws are satisfied at the same time.

  1. No validation of reference strings

  2. A structure that lets user input overwrite internal context objects

  3. A design that connects this polluted object to the thenable chain

4.1 Step 1 – A Structure That Trusts Reference Strings As-Is

Flight uses a string such as "1:foo:bar" to mean “follow the path .foo.bar from the value of the chunk with ID 1.” The problem is that the keys appearing in this path were not filtered.

An attacker can pass a string like the following as-is.

The logic that processed it looked like this.

function followRef(response, refString) {
  const parts = refString.split(':');
  const id = parseInt(parts[0], 16);    // chunk ID (hex)

  let obj = response.chunks[id].value;

  for (let i = 1; i < parts.length; i++) {
    obj = obj[parts[i]];                 // property access without filtering
  }

  return obj;
}

const Fn = followRef(response, "1:__proto__:constructor:constructor");
// Fn === Function

As a result, a simple string input alone can reach the Function constructor.

4.2 Step 2 – Polluting Internal Context Objects (Context Pollution)

The Flight deserializer recursively traverses the whole model to restore strings, arrays, and objects.

Inside React, the following fields are used as context object properties.

  • _response

  • _formData

  • _prefix

  • status

  • then

If an attacker simply includes these keys in the JSON payload, the internal fields of the context object are replaced with attacker values after deserialization.

After deserialization, the internal state is polluted as follows.

  • _response : an attacker object instead of the original context

  • _formData : an arbitrary object instead of a real FormData

  • _prefix : a string later used as part of code

  • status, then : fields used for async flow control are also changed to attacker values

On top of this, the Function constructor obtained in Step 1 can be injected into _formData.get.

const Fn = Function; // assume this is the result of Step 1
revived._formData.get = Fn(
  "key",
  "return process.mainModule.require('child_process').execSync('id');"
);

Now the point at which internal logic calls _formData.get(prefix + id) becomes the code execution point.

4.3 Step 3 – Blob Token Handling + Automatic Execution of the Thenable Chain

The Blob token handling process looks like this.

function resolveBlobToken(response, token) {
  const idHex = token.slice(2);   // e.g., "1337"
  const id = parseInt(idHex, 16); // 0x1337

  const key = response._prefix + id;
  return response._formData.get(key);
}

As shown earlier, this function is designed so that on the reviveModel() → parseModelString() path, whenever a string matches the $B… pattern, it is called.

In other words, if a token such as $B1337 exists in the Flight model, then at the point the chunk is finally “interpreted as a model,” a _formData.get(_prefix + id) call is guaranteed to happen once.

// state set by the attacker
response._prefix = "return process.mainModule.require('child_process').execSync('id'); // ";
response._formData.get = Function;

// Blob token handling
resolveBlobToken(response, "$B1337");
// → Function("return process.mainModule.require('child_process').execSync('id'); // 4919")();

This logic is called as part of processing Flight chunks, i.e., as part of the thenable chain.

  1. Flight chunks are designed like thenables

  2. When a chunk reaches a certain state, React processes it like Promise.resolve(chunk)

  3. Per the JS standard, a thenable chunk then has chunk.then(...) called automatically

  4. Inside then, reviveModel() runs

  5. When a Blob token ($Bxxxx) is found, resolveBlobToken() is called

  6. _formData.get(_prefix + id) runs

If _formData.get has already been polluted with Function in the previous step, this call immediately leads to code execution in the form of Function(_prefix + ...)().

5. Summary: A Framework Is Not a “Trusted Black Box”

React has been used reliably for a long time, and many developers have built without problems without looking deeply into its internals.

React2Shell, however, broke that trust.

  • Framework internal context can be exposed to attacker input

  • Automatic execution rules can become unexpected vulnerabilities

  • The Flight parser can reach the Function constructor with a single string

Key takeaways from this incident:

“A framework is ultimately code written by people, and it cannot be placed outside the trust boundary.”

“Abstractions we do not understand are gifts to attackers.”

🔜 Next (Part 3): “After React2Shell: How Should Companies Rebuild Their Response Framework?”

Part 3 goes beyond simple scanning to explain what security framework organizations should build in light of this incident.

It covers why an SBOM/VEX-based response framework is necessary, and how companies can build response capabilities that hold steady when large-scale framework vulnerabilities recur.

Z ZIEN
{# 쿠키 수집 안내 팝업 — 선택 전 방문자에게만(cookie-consent '쿠키 안내 팝업'). 버튼은 폼 POST 라 JS 없이 동작하고, 닫기(×)는 '거부' 와 같이 저장된다. source·close 는 누른 버튼별 집계용 — 닫기를 거부와 따로 센다(cookie-consent '쿠키 선택 집계'). #}