深入了解 React2Shell:对未公开协议的深度剖析如何导致 RCE

发现一个关键漏洞很少是一个线性过程。通常,它是执着的好奇心、深入研究未公开内部机制的意愿,以及在经历了一系列错误的尝试和突破后的坚持的结果。这正是 "React2Shell" (CVE-2025-55182) 的情况,这是一个影响了数百万使用 React 和 Next.js 的网站的远程代码执行 (RCE) 漏洞。

React2Shell 并不是针对 React 中的某个 bug 进行有目的的搜索,而是为了理解 "Flight" 协议——这是 React 用于促进 React Server Components (RSC) 和 Server Functions 的机制。 通过拆解该协议,一名安全研究人员发现了一条从简单的属性访问到完全控制系统的路径。

理解 Flight 协议

为了实现客户端和服务器端 JavaScript 之间的无缝交互,React 团队开发了一种名为 "Flight" 的协议。与标准 JSON 不同,Flight 允许传输复杂的数据类型——例如 DateBigIntMapSet——以及循环引用和异步 Promises

Flight 消息被分解为 "chunks",并使用特定的语法(例如,$D 用于日期,$x 用于引用)来表示类型。然而,正如研究人员和社区成员所指出的,该协议本质上是未公开的。这种规范缺失意味着大多数安全审计人员将 Flight 负载 (payload) 视为简单的 JSON,从而忽略了大量的攻击面。

第一道裂缝:原型污染与类型强制转换

通往 RCE 的旅程始于一个简单的观察:Flight 允许引用对象的属性,包括其原型上的属性。通过发送一个引用了 Number.prototype.toString 的负载 (payload),攻击者可以将一个原生 JavaScript 函数放置在攻击者可控的对象上。

虽然这看起来像是一个小疏忽——Vercel 创始人 Guillermo Rauch 将其描述为 "一个明显的安全检查缺失"——但它为类型强制转换攻击打开了大门。在许多 Next.js 应用中,开发者使用 TypeScript 来定义 Server Functions 的预期类型。然而,TypeScript 仅提供构建时分析;它并不在运行时强制执行类型。

如果开发者预期接收到一个字符串,但实际上接收到了一个带有恶意 toString 方法的、由 Flight 构建的对象,服务器可能会在字符串拼接过程中隐式地调用该方法,从而导致意外的代码执行路径。

突破口:滥用 "thenables"

实现完全 RCE 的路径需要一种能够使用可控参数执行函数的方法。突破来自于对 JavaScript 如何处理 async/await 和 "thenables" 的理解。 \n在 JavaScript 中,任何具有 .then() 方法的对象都被视为 "thenable"。当在 thenable 上使用 await 关键字时,JavaScript 引擎会自动调用 .then() 方法。至关重要的是,如果一个 thenable 解析为另一个 thenable,引擎将递归地调用 .then(),直到达到一个非 thenable 的值。

通过构建一个 Flight 负载 (payload),其中 then 属性被映射到像 Array.prototype.push 这样的函数,研究人员发现 React 内部的 await decodeReply(...) 会自动触发攻击者提供的函数。这为隐式调用函数提供了一种原始手段。

升级为远程代码执行

实现 RCE 的最终飞跃涉及访问 React 的内部 Chunk 对象。通过使用 $@x 语法,攻击者可以创建一个 chunk 的 promise。通过引用 Chunk.prototype.then,研究人员能够迫使 React 运行其自身的内部 .then 实现来处理攻击者控制的对象。

这使得研究人员能够伪造 Chunk 的内部状态,包括 "server manifest"——即告诉 React 在给定 Server Function ID 时应该执行哪个模块和函数的映射表。最初,研究人员尝试将 ID 映射到 Node.js 的 child_process.exec,但遇到了 Webpack/Turbopack 处理模块加载时的限制。

最终,一个优雅的漏洞利用链条包含:

  1. 使用 Chunk 内部机制来触发一个带有可控参数的函数调用。
  2. 利用 $Bx 代码(用于文件 blob 引用)来使用受控参数调用 _formData.get(...)
  3. 构建一个恶意函数并将其放置在最终的 then 属性上,以触发最终的执行。

披露与后续影响

一旦开发出概念验证 (PoC),复现过程非常简单:安装一个全新的 Next.js 应用,运行该脚本,并实现 RCE。Meta 在 17 小时内完成了分级并确认了该漏洞,并与 Vercel 以及 Cloudflare 等 WAF 提供商紧密合作,在公开披露之前实施了防御措施。

经验教训

React2Shell 事件凸显了几个关键的安全教训:

  • 未公开协议的危险性: 当一个框架引入自定义通信协议来处理复杂类型时,它会创建一个无法使用标准工具进行审计的全新攻击面。

  • 类型安全的错觉: TypeScript 的构建时检查并非运行时验证的替代品。在处理不受信任的输入时,假设一个变量是字符串是因为类型注解,这是一种危险的假设。

  • 现代框架的复杂性: 随着框架不断模糊客户端与服务器端代码的界限,为了提升开发者体验,内部 "plumbing"(管道/内部机制)的复杂性也随之增加,这往往会引入微妙的漏洞,只能通过深度的、系统性的分析才能发现。

正如一位社区成员所指出的,该漏洞是一个更大架构风险的症状:“...通过模糊客户端代码与服务器端代码的界限...并允许协议序列化代码而不仅仅是原始类型。”

Sources