CVE-2025-55182 - React2Shell 分析笔记
CVE-2025-55182: React2Shell
CVE-2025-55182(俗称 React2Shell)是一项于 2025 年 12 月 3 日公开披露的 严重远程代码执行(RCE)漏洞,影响 React Server Components(RSC)及其 Flight 协议实现。该漏洞因 不安全的反序列化 允许未经认证的攻击者通过精心构造的请求触发服务器端任意代码执行,官方给出 CVSS 10.0(最高严重度),被列为顶级危急缺陷。公开披露后,React 官方和主要框架生态(包括 Next.js)迅速发布补丁版本(如 React RSC 包的 19.0.1/19.1.2/19.2.1 及 Next.js 的多个修复版本),并强烈建议尽快升级以避免风险。该漏洞影响广泛:凡使用受影响 React RSC 版本和默认配置的生产应用均可能暴露,包括大量 Next.js 网站和云服务环境,安全厂商观测到披露后数小时内出现扫描和主动利用行为,并被 CISA 纳入已知被利用漏洞目录,成千上万公开 IP 仍易受攻击。
Docker 搭建靶场
文件准备
先装一个存在该漏洞的版本, 例如 15.5.6:
1 | npx create-next-app@15.5.6 nextjs-1 --yes |
进入刚刚的项目文件夹, 新建 dockerfile:
1 | # 使用当前 Next.js 推荐的 Node 运行时 |
新建 docker-compose.yml:
1 | version: "3.9" |
构建容器
1 | docker-compose up --build |
访问 9229 端口:

访问 3000 端口:

注意, 需要设置
ENV NODE_OPTIONS="--inspect=0.0.0.0:9229", 如果不在 docker 内则需要设置export NODE_OPTIONS="--inspect=0.0.0.0:9229";
网站主要用到的端口:
- 3000: Web 应用端口;
- 9229: dev 服务器的守护进程端口;
- 9330: 应用进程调试端口;
调试
用 Google Chrome 调试配置:
在 Chrome 浏览器中输入 chrome://inspect 并开启 9230 端口到远程地址, 然后重启容器:

之后再在 Chrome DevTools 中禁用忽略列表 (Enable Ignoring Listing), 就能看到了:


原理简述
Promise 对象
链式调用
引用自知识星球 P 神的博客;
早期的 JS 代码中含有大量函数嵌套, 使得维护调试困难, 代码不易复用, 例如:
1 | // BEFORE: 嵌套回调 - 多个独立上下文,变量需手动传递 |
因此从 ES6 开始, javascript 引入了 Promise 这个对象。Promise 对象将函数嵌套转换为链式调用, 例如:
1 | // AFTER: Promise链 - 统一上下文,线性作用域 |
就像 Spring 的 bean 链式构建一样, Promise 对象可以维护统一的方法 (then/catch) 并持续 return 一个 Promise。
有限状态机
观察这段变化:
1 | // BEFORE |
可以看出, Promise 只存在 3 个状态:
- pending
- fulfilled
- rejected
状态机内部调度, then() 的返回值会决定下一环的执行:
| then 的返回值 | 下一环收到的 |
|---|---|
| 普通值 | 直接进入 fulfilled |
| Promise | 等它 resolve 后继续 |
| 抛异常 | 自动进入下一环 catch() |
Thenable
为了保证对老代码的兼容, JS 引入了 thenable 这个概念: 任何对象只要有 then() 这个方法, 就是一个 thenable 对象; 它可以替代 Promise 的功能,在 await 等需要 Promise 的位置使用 (在 then() 里写一个闭包)。
安全隐患
Promise 的设计初衷是可维护性, 但其统一接口 + 可组合性为攻击链构造提供了便利; 控制流全部由 then / catch 维护, 使得代码更加容易挂载, 并且更好预测。
结合 Thenable 协议, 任何对象 (即使非 Promise), 只要 Key 值可以被用户控制, 那么就可以伪造一个 Thenable 对象;
攻击者甚至不需要真的构造 Promise, 只要一个对象带 then 属性即可劫持执行链。
React Server Components (RSC)
RSC 是 React 18 引入的新范式, 允许组件在服务端渲染并将结果流式传输到客户端。
1 | ┌─────────────────────────────────────────────────────────────────┐ |
如图所示, 与传统 SSR 不同, RSC 可以直接访问服务端资源而无需 API; 保持了交互性使客户端可以无缝协作
React Server Functions
React Server Functions (或 Server Actions) 允许客户端像调用本地函数一样调用服务端函数:
调用例
- Server Function 定义方式
1 | // app/actions.js |
- 客户端调用方式
1 | // app/page.jsx (Client Component) |
底层通信
Flight 协议实际上就是给通信加了一层壳, 这个壳的作用和 Python 中的 pickle 包是类似的, 定义了发送, 解析的格式:
sequenceDiagram
participant Client as 客户端组件 (浏览器)
participant Flight as Flight 序列化器 (客户端)
participant HTTP as HTTP POST 请求
participant Server as Server Function 执行器
participant FlightSrv as Flight 序列化器 (服务端)
participant React as React 渲染器 (客户端)
Note over Client: 用户调用 submitForm(formData)
Client->>Flight: 1. 将参数序列化为<br/>Flight 协议格式
Flight->>HTTP: 2. 发送 HTTP POST 请求
HTTP->>HTTP: 3. 添加请求头:Next-Action: action-id
HTTP->>HTTP: 4. 请求体为 multipart/form-data<br/> (包含 Flight 数据块)
HTTP->>FlightSrv: 5. 服务端反序列化 Flight 数据
FlightSrv->>Server: 6. 执行对应的 Server Function
Server->>FlightSrv: 7. 将返回值序列化为 Flight 格式
FlightSrv->>React: 8. 客户端反序列化并更新 React 组件树
Server Action 请求格式
1 | POST /page-url |
| Header | 功能 | 特点 | 示例 |
|---|---|---|---|
Next-Action |
Server Action 的唯一标识符 | 40 字符哈希 | 1218dsu2132dd.. |
Content-Type |
内容类型 | 只能是 multipart/form-data |
multipart/form-data; boundary=... |
Next-Router-State-Tree |
路由状态 | 可选项 | [encoded] |
Action ID 生成
逻辑简化:
1 | // Next.js 内部生成逻辑 (简化) |
需要注意的是, 对于 Next.js 15+, 这里的哈希不是 SHA1/MD5 ,而加密、非确定性的标识符, 这些标识在不同构建之间可能变化。在编译时生成,并在构建之间周期性重新计算以增强安全性。
旧版本中可能是基于 SHA-1
React Flight 协议
Filght 协议是一个自定义流式序列化协议, 其作用就和 php, python pickle 中的序列读取类似;
| 目标 | 说明 |
|---|---|
| 流式传输 | 支持边渲染边传输, 无需等待完整响应 |
| 引用共享 | 相同数据只传输一次, 通过 ID 引用 |
| 类型保留 | 保留 React 特有类型 (Promise、组件、函数引用等) |
| 紧凑高效 | 比纯 JSON 更紧凑, 减少传输体积 |
| 双向支持 | 服务端→客户端 (渲染) 和客户端→服务端 (Server Actions) |
紧凑高效
Flight 支持分块引用:
1
2
3
4
5 // 普通 JSON - 无法表示引用、Promise、函数等
{
"user": {"name": "Alice"},
"posts": [{"author": {"name": "Alice"}}] // user 重复传输
}
1
2
3
4 // Flight 协议 - 支持引用共享
0:{"name":"Alice"} // Chunk 0: user 对象
1:{"author":"$0"} // Chunk 1: 引用 Chunk 0
2:{"user":"$0","posts":["$1"]} // Chunk 2: 根对象
Flight 数据格式
Flight 协议的数据格式:
1 | <id>:<type>{<map> (键值对)} |
常见 Type:
| Type | 说明 | 用例 |
|---|---|---|
<空> |
模型数据 (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"} |
一个完整的 Filght 响应示例:
1 | 0:I{"id":"./app/page.js","name":"default","chunks":["app/page"]} |
Chunk 状态机
Chunk 也是一个状态机, 具有和 Promise 对象相同的三个状态: - pending, fulfilled, rejected (因为 Chunk 本质上就是继承 Promise 对象), 在此基础上, 添加了两个新状态:
1 | ┌─────────────────────────────────────────────────────────────────────────┐ |
Chunk 内部数据结构
1 | // React 源码: packages/react-client/src/ReactFlightClient.js |
Response 对象
Flight 解析会话创建一个 Response 对象, 管理所有 Chunk:
1 | function createResponse(bundlerConfig, formData, prefix) { |
特殊变量
$ 前缀系统来表示特殊值。
常见字符表
| 前缀 | 类型说明 | 示例 | 含义描述 | 解析方式 |
|---|---|---|---|---|
$ |
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 | // 语法: "$<chunkId>:<key1>:<key2>:..." |
解析源代码:
1 | function parseModelString(response, parentObj, key, value) { |
漏洞点
$与$@的解析区别:
1 | // 假设 Chunk 0 的原始数据是: '{"name": "Alice"}' |
$@ 返回的是一个完整的 Promise 对象, 包含 __proto__;
Chunk.__proto__-ReactPromise.prototype
Chunk.__proto__.then-ReactPromise.prototype.then
Chunk.__proto__.constructor=Object->Function
getChunk
1 | function getChunk(response, id) { |
可以从包里直接构造 Chunk; data 是完全可控的;
initializeModelChunk
1 | function initializeModelChunk(chunk) { |
利用 $ 执行特殊处理即可, 攻击者通过 FormData 提供的数据会被直接用于创建 Chunk,data 参数完全可控。
ReactPromise.prototype.then- thenable 接口
1 | ReactPromise.prototype.then = function(resolve, reject) { |
注意: then 方法会在一个 thenable 对象被 await 使触发, 进一步只要在构造 chunk 时编辑其状态为 resolved_model, 即可触发解析;
从这一步开始进入了 Promise (Thenable) 对象的链式解析, 只要有一处完成注入, 理论上后续内容全部可以注入;
react-server-dom-webpack 包
1 | // 简化的漏洞代码 |
攻击链
服务器在处理 Flight 请求时会按顺序解析客户端提供的 chunk map。由于 chunk ID 与 chunk value 均完全由客户端控制, 考虑这个构造:
1 | chunk1 = "$@0" |
$@0 会令服务器将 chunk1 的值解析为 chunk0 (thenable) 对象本身
React Flight 解析器在解引用时会自动执行:
1 | chunk0.then(resolve, reject) |
最终触发 payload 中植入的任意逻辑; 配合上 __proto__ 的组合拳, 就可以完成原型链污染;
攻击原语
| 键路径 | 值 | 作用描述 |
|---|---|---|
$1:__proto__:then |
Chunk.prototype.then |
让伪造对象成为合法 thenable |
$1:constructor:constructor |
Function |
动态创建并执行代码 |
$1:__proto__:constructor |
Object |
获取 Object 构造函数 |
$1:__proto__:constructor:prototype |
Object.prototype |
访问所有对象的原型 |
攻击图示
基本流程
sequenceDiagram
participant C as 客户端(攻击者)
participant S as React Server(Flight Parser)
participant T as Chunk Table
C->>S: 发送 chunk map 1="$@0" 0={then: payload}
S->>T: 写入 chunk1="$@0" chunk0=thenable
S->>S: 解析 chunk1 → $@0
S->>T: 取出 chunk0(原始对象)
S->>S: 检测到 chunk0.then 存在
S->>T: 执行 chunk0.then(resolve, reject)
T->>S: payload 执行(原型链污染 / RCE)
S->>C: 返回被攻击后的响应
HTTP 攻击 payload 示例
1 | POST / |
源代码层面的完整流程
- 预认证反序列化链
sequenceDiagram
autonumber
participant C as 客户端
participant A as Next.js<br>handleAction
participant F as Flight<br>decodeReply
participant G as getChunk(0)
participant R as ReactPromise(Chunk0)
C->>A: POST /\nHeader: next-action: x\nBody: FormData(chunks)
A->>A: 发现是 Server Action 请求
A->>F: decodeReplyFromBusboy(formData)
F->>G: getChunk(0)
G->>G: 从 FormData 读取 chunk0.value = 恶意 JSON
G->>R: 创建 ReactPromise(status="resolved_model")
R-->>F: 返回 Chunk0
F-->>A: 返回 Chunk0 (thenable)
A->>R: await Chunk0<br>⇒ 调用 Chunk0.then(resolve, reject)
- 第一次 then 链 (解析
$1:__proto__:then)
sequenceDiagram
autonumber
participant R0 as Chunk0
participant INIT as initializeModelChunk
participant PARSE as parseModel
participant STR as parseModelString
participant G1 as getChunk(1)
participant R1 as Chunk1
R0->>INIT: ReactPromise.prototype.then()
INIT->>PARSE: parseModel(恶意 JSON)
PARSE->>STR: 解析 "$1:__proto__:then"
STR->>G1: getChunk(1)
G1->>R1: 解析 "$@0" → 返回 Chunk0 本身
STR->>STR: 遍历 "__proto__" → ReactPromise.prototype
STR->>STR: 获取 "then" → ReactPromise.prototype.then
STR-->>PARSE: 返回真正的 then 方法
PARSE-->>INIT: 返回 fakeChunk(伪造对象)
INIT-->>R0: chunk0.value = fakeChunk
R0->>R0: await fakeChunk<br>⇒ 再次调用 ReactPromise.prototype.then
- 第二次 then 链 (解析
$B1337-> function)
sequenceDiagram
autonumber
participant F0 as fakeChunk
participant INIT as initializeModelChunk
participant PARSE as parseModel
participant STR as parseModelString
participant FD as fakeChunk._response._formData
participant FN as Function 构造函数
F0->>INIT: ReactPromise.prototype.then.call(fakeChunk)
INIT->>PARSE: parseModel('{"then":"$B1337"}')
PARSE->>STR: 解析 "$B1337"
STR->>FD: _formData.get(prefix + "1337")
FD->>FN: Function("恶意代码...")
FN-->>STR: 返回恶意函数 maliciousFunction
STR-->>PARSE: 返回 { then: maliciousFunction }
PARSE-->>INIT: 初始化完成
F0->>F0: await result<br>⇒ result.then(resolve, reject)
- 最终 RCE - Function 执行
sequenceDiagram
autonumber
participant RES as result({then: maliciousFunction})
participant MF as maliciousFunction
participant OS as Node.js child_process
RES->>MF: await result ⇒ 调用 maliciousFunction()
MF->>OS: execSync("id")
OS-->>MF: 返回命令执行结果
MF-->>RES: throw Error(命令输出)
注意这个流程中 React 先反序列化 Flight payload, 再验证 next-action (Action ID), 而刚刚的分析中已经指出这个序列化是不安全的, 因此服务器将可能解析恶意 chunk。
在此基础上, 利用 $@ 自引用 + 原型链路径穿透 的组合拳最终拿下 RCE。
补丁/修复
对现有 React 的最佳修复方案是立刻恢复快照, 然后更新到最新的补丁版本;
官方补丁修复点
最关键的修复点是, 添加 hasOwnProperty 检查:
1 | // packages/react-server-dom-webpack/src/ReactFlightServerReference.js |
hasOwnProperty 会强制用户只能访问对象本身的字段, 而非继承 (extends) 的属性, constructor, __proto__, toString 等大量预留属性都属于继承;
总结
React2Shell 利用 Flight 反序列化对用户可控 Chunk 的链式访问缺乏限制,攻击者可通过 $@ 获取任意 Chunk 对象并污染原型链. 伪造带恶意 then 的 thenable. 使服务器在解析 Promise-like 时执行任意代码, 从而实现远程代码执行。
此外, 后端开发必须要知道的是, 永远不能相信前端传回的任何内容。
贴上一个有意思的 b 站视频:
参考博客/文章
📄 免责声明:
- 本文所涉及的安全技术、漏洞分析及相关示例仅用于网络安全研究、教育与防御目的。请勿在未授权的系统、设备或环境中复现、利用或传播文中任何技术细节。
- 任何基于本文内容从事的违法行为均与作者无关,由行为人自行承担全部法律责任。请严格遵守相关法律法规,合理合法地使用本文信息,以促进更安全的技术生态。



























































