Next.js 在 App Router 中引入的 Server Actions 功能极大地简化了前后端交互流程,但其底层使用的通信协议——React Flight 协议,被发现存在严重的安全漏洞。该漏洞允许攻击者通过精心构造的 Multipart 数据包,绕过安全检查,最终在服务器端实现远程代码执行 (RCE) 。
POC 复现 gist.github.com/maple3142/4…
Server Actions 与 Flight 协议机制
为了理解漏洞,首先需要了解 Server Actions 的运行机制。
Server Actions 的设计初衷
在 Next.js 13+ 中,Server Actions 允许开发者定义服务端函数,并直接在客户端组件中调用,无需显式创建 API 路由。
// app/actions.js
'use server'
export async function submitForm(formData) {
// 该函数在服务端运行
await db.users.create({ name: formData.get('name') })
return { success: true }
}
// app/page.jsx (客户端组件)
import { submitForm } from './actions'
export default function Page() {
return (
<form action={submitForm}>
<input name="name" />
<button type="submit">提交</button>
</form>
)
}
底层通信:Flight 协议
当用户触发上述表单时,浏览器并非发送普通的 JSON 请求,而是通过 React Flight 协议进行通信。且关键在于,为了支持文件上传等特性,传输格式通常为 multipart/form-data。流程如下:
- 序列化:React 将客户端数据序列化为 Flight 协议格式。
- 传输:通过 HTTP POST 请求发送,请求头包含
Next-Action标识符,Content-Type 为multipart/form-data。 - 反序列化:Next.js 服务端接收请求,解析 Multipart 数据中的 Flight 格式 Payload。
- 执行与响应:执行服务端函数,并将结果再次通过 Flight 协议编码返回给客户端。
漏洞的核心,正是出现在服务端解析和反序列化这些 Chunk 的过程中。
漏洞成因:攻击链的三环
该 RCE 漏洞的利用过程并非单一缺陷导致,而是串联了三个逻辑漏洞。
第一环:引用解析中的路径遍历
Flight 协议允许使用冒号 : 对引用对象的属性进行嵌套访问。例如,$0:users:0:name 表示访问 Chunk 0 中 users 数组第 0 项的 name 属性。
服务端解析逻辑的简化伪代码如下:
// 服务端解析逻辑示意
let value = chunk.value;
const path = reference.split(':'); // 分割路径
for (let i = 1; i < path.length; i++) {
value = value[path[i]]; // 逐层访问属性
}
漏洞点:服务端并未对路径中的属性名进行过滤或白名单验证。
这意味着攻击者可以构造特殊路径来访问 JavaScript 对象的原型链属性:
$1:__proto__:then:访问原型链上的then方法,用于劫持 Promise 解析。$1:constructor:constructor:获取对象的构造函数的构造函数,即Function构造函数。
第二环:伪造 Chunk 注入
在 React 内部实现中,Chunk 被视为一种类似 Promise 的对象。服务端在解析时,会检查对象的 status 属性。如果 status 标记为完成状态(如 "resolved_model"),服务端会直接解析其内容,而不会验证该对象是否由系统生成。
攻击者可以构造一个伪造的 Chunk 对象作为 Payload 发送给服务器。根据 PoC 显示,攻击者利用 value 字段作为触发器:
// 攻击者构造的恶意 Chunk (Part "0")
{
"then": "$1:__proto__:then", // 1. 劫持 then 属性
"status": "resolved_model", // 2. 伪装成已解析状态
"reason": -1,
// 3. 关键触发点:Value 中包含对 Blob ($B) 的引用
// 这会导致解析器尝试去 "获取" 这个 Blob,从而触发下方被替换的 get 方法
"value": "{"then":"$B1337"}",
"_response": { // 4. 注入恶意的内部 Response 对象
"_prefix": "process.mainModule.require('child_process').execSync('xcalc');",
"_formData": { "get": "$1:constructor:constructor" } // 待替换的函数占位符
}
}
由于缺乏严格的类型验证,服务端会接纳这个伪造的对象及其包含的恶意内部属性 _response。
第三环:Function 构造函数注入与代码执行
攻击者的最终目标是利用上述两个漏洞,将代码字符串转换为可执行函数。
- 替换关键方法:攻击者利用第一环的“路径遍历”,将伪造 Chunk 中
_response._formData.get的值指向$1:constructor:constructor(即Function构造函数)。 - 触发执行:攻击者在 Payload 的
value字段中包含一个$B(Blob) 类型的引用。
当服务端解析 value 中的 $B 类型引用时,会执行如下逻辑:
// React 源码逻辑简化
case 'B': {
const prefix = response._prefix; // 攻击者注入的代码字符串
const blobKey = prefix + id;
// 关键点:调用 _formData.get 以获取 Blob 数据
// 此时 get 已被替换为 Function 构造函数
const backingEntry = response._formData.get(blobKey);
return backingEntry;
}
上述代码实际上等效于执行了:
new Function("process.mainModule.require('child_process').execSync('xcalc');" + "1337")
这会创建一个匿名函数,其函数体即为攻击者注入的代码。随后,该函数被作为 Promise 的回调执行,从而在服务器端完成了远程代码执行 (RCE) 。
攻击流程总结
整个攻击过程可以概括为以下步骤:
- 构造 Payload:攻击者通过
multipart/form-data发送包含伪造 Chunk 的恶意数据包。 - 原型链污染:利用路径遍历漏洞(
__proto__),劫持 Promise 解析过程。 - 构造函数替换:利用路径遍历获取
Function构造函数,替换掉内部的_formData.get方法。 - 伪造对象注入:服务器反序列化伪造的 Chunk,加载恶意的
_response对象。 - 代码编译与执行:服务器解析
value中的 Blob 引用时,错误地调用Function构造函数,将恶意字符串编译为函数并在服务端执行。
总结
防御措施:
打开这篇文章复制进 cursor 诊断项目的package.json 生成依赖升级命令
评论: 这是 FE 发展至今对全球互联网影响最大的 Bug,无数一键部署到 Vercel ,Netlify, Cloudflare 的 NextJS 网站都是直接暴露的可攻击端点。
所幸 NextJS 这一套纯 FaaS 方案其实私有化部署到自己服务器并不方便,所以互联网上大部分都是部署到 Vercel 自己服务或免费 FaaS平台的,也是自我得之自我失之了。
坏处就是免费的 FaaS 服务被影响最大,比如 CloudFlare 在处理本问题时 2025/12/5 又出现了全球宕机。虽然我是 CF 粉,但这个洗不了,股价还有下跌空间。
附 WAF 侧如何配置
Cloudflare WAF 中安全拦截
当 WAF 试图使用包含大量回溯的正则表达式(ReDoS 风险)去匹配一个极其复杂的恶意 Payload 时,CPU 会被瞬间占满。对于 Next.js 这个漏洞,攻击 Payload 结构复杂且嵌套深,如果我们试图用正则去精准匹配整个 JSON 结构,极易重蹈覆辙。
配置思路
我们不需要解析整个 JSON,只需要抓住攻击链中必不可少的特征字符串。
-
流量筛选:仅检查带有
Next-Action请求头的POST请求。这能过滤掉 99% 的普通流量,极大降低 WAF 负载。 -
特征识别:
__proto__:这是原型链污染的核心,正常业务中几乎不可能在表单 Key 中出现此字符串。constructor:这是获取 Function 构造函数的关键。真实 PoC 显示攻击者必须调用:constructor来获取构造器。为了防止误杀(如用户输入 "constructor" 单词),建议匹配带冒号的引用格式(:constructor或"constructor"),但在高危时期,阻断裸字符串是更稳妥的选择。
Cloudflare WAF 配置步骤
- 登录 Cloudflare 控制台,进入目标域名的 Security (安全性) > WAF (Web 应用程序防火墙) 。
- 点击 Create rule (创建规则) 。
- 点击 Edit expression (编辑表达式) ,切换到表达式模式。
- 将下方的表达式粘贴进去:
Bash
(http.request.method eq "POST" and any(http.request.headers.names[*] eq "next-action") and (http.request.body contains "__proto__" or http.request.body contains ":constructor"))
规则详解与安全性分析
-
http.request.method eq "POST":- 限制仅检测 POST 请求。
-
any(http.request.headers.names[*] eq "next-action"):- 这是 Next.js Server Actions 的特征指纹。如果不包含此头,说明不是 Server Action 请求,直接放行。
-
http.request.body contains "__proto__":- 使用
contains运算符。底层实现通常是 BM 算法或类似的高效字符串查找算法,时间复杂度接近 O(n),不存在回溯风险,不会导致 CPU 飙升。
- 使用
-
http.request.body contains ":constructor":- 这里加了一个冒号
:。在 Flight 协议中,引用通常通过冒号分隔(如 PoC 中的$1:constructor:constructor)。加上冒号可以有效避免误伤正文中包含 "constructor"(建筑商)等普通单词的提交,同时又能精准命中攻击 Payload。
- 这里加了一个冒号
配置建议:设置好规则后,建议先将动作设置为 Log (记录) 观察一段时间,确认没有误拦截正常业务后,再调整为 Block (阻断) 。