如何 129KB 的空白(以及递归循环)导致网络崩溃

发布: (2025年12月13日 GMT+8 12:00)
8 min read
原文: Dev.to

Source: Dev.to

Cover image for How 129KB of Whitespace (and a Recursive Loop) Broke the Web

React2Shell (CVE‑2025‑55182) 公布以来已经过去大约一周。最初的“全部下线”恐慌基本平息,PagerDuty 的警报也 hopefully 已经不再疯狂响起。烟雾散去后,我们终于可以喘口气,审视这场灾难,弄清楚 React 生态到底发生了什么。

对我而言,事情的严峻性在收到 8 封来自 GCP(Google Cloud)的邮件时彻底敲响警钟。这并不是普通的计费警报,而是这样的内容:

New Advisory Notification
Dear Google Cloud customer,
You’ve received an important Google Cloud notification affecting your resource…
Notification Title: Important Security Information Regarding React & Next.js Vulnerability (CVE‑2025‑55182)

当你的云服务商开始大量发送标记为“Advisory Notification”的邮件,并且提到某个 JavaScript 框架时,你就会意识到这不仅是个 bug,而是一次 事件!

我们是如何走到这一步的

要理解漏洞本身,需要先看看整体架构。多年来我们一直在推动客户端与服务端的“无缝”集成。我们希望 React Server Components (RSC) 能够在后端直接获取数据并流式传输到前端。

但这里有一个我们很少提及的权衡点:信任边界

在过去(字面意义上 12 个月前!)我们大多数情况下只在前后端之间来回传递 JSON。JSON 安全是因为它“愚蠢”——它只是数据。RSC 需要传输“执行上下文”(比如 Promise、Symbol、Server Actions),而 JSON 无法容纳这些,于是 React 开发了 Flight 协议。

Flight 的致命缺陷

漏洞出现在 react-server-dom-* 包处理 Flight 协议的方式上。按照设计,Flight 允许服务器对客户端发送的复杂对象进行反序列化。

如果你了解安全史,“deserialize” 这个词本身就该让人打个寒颤。Java(Struts)、PHP、Python 都曾在此遭遇灾难性失败。React2Shell 证明了 JavaScript 也并非免疫。

该漏洞使得未认证的攻击者能够发送特制的 HTTP 请求,专门操纵类似 Promise 的对象(即 “thenables”) 到服务器。React 的内部逻辑会积极尝试“解析”这个恶意对象,从而让攻击者劫持执行流并运行任意代码

WAF 绕过(为何邮件来得太迟)

本周最让人恼火的事情之一,就是看到我们本以为可靠的安全防线失效。我们原本以为 Web 应用防火墙(WAF)会拦截此类攻击,结果并没有。

攻击者发现大多数 WAF 为了提升速度,只检查请求体的前 8 KB 到 128 KB。于是他们使用了一个极其简单的技巧:填充

他们在恶意负载的开头加入约 129 KB 的“垃圾”数据(空白、注释)。WAF 扫描到这些填充内容,认为没有问题,便将请求转交给 Next.js 服务器。而服务器会读取整个请求体,随后对负载进行反序列化,触发远程代码执行。

第二波:不仅仅是 RCE

当你刚刚为修复 RCE 漏洞而松一口气时,安全研究员(以及 React 团队)发现这只是一根更深的兔子洞。12 月 11 日我们得知解析器不仅仅易受代码执行攻击,还会因结构性滥用而崩溃。这导致了两个你现在必须了解的全新 CVE。

1. 无限循环(CVE‑2025‑55184 & CVE‑2025‑67779)

Flight 协议的反序列化器本质上是递归的——它必须如此才能解析引用中的引用。如果你发送的负载中某个块以特定方式引用自身,Node.js 进程会进入同步的无限循环。

由于 Node.js 是单线程的,这后果相当灾难性。CPU 会飙升至 100 %,服务器瞬间对所有用户失去响应

在无服务器环境(Vercel、AWS Lambda)中,这会导致我们所谓的 “Denial of Wallet”(钱包拒绝服务)。攻击者可以迫使你的函数一直运行直至超时,进而启动成千上万的满负荷实例,产生巨额计算费用——这些费用实际上并未被真正使用。

2. 反射中的间谍(CVE‑2025‑55183)

这个更为阴森。它允许攻击者诱骗服务器泄露自身的源代码

如果你的 Server Actions 对参数使用 toString()(或隐式转换),攻击者可以传入特制的引用对象,使内部闭包状态序列化回客户端。

如果你遵循最佳实践,仅使用环境变量(例如 process.env.DB_PASS),基本上是安全的——攻击者只能看到变量名而非其值。但如果你在代码中硬编码了 API 密钥或其他机密信息,这些现在已经公开。

“补丁的补丁”

当 DoS 漏洞首次被发现时,React 发布了 19.0.2 版本。大家都升级了,认为已经安全。

随后研究员发现可以通过在循环引用上再加一层间接性来绕过该修复,这迫使出现了 第二轮 补丁。如果你只修复了 RCE 而止步于此,仍然会受到 DoS 与源码泄露缺陷的影响。

接下来该怎么做

如果你在过去 24 小时内还没有升级,基本上已经在借用时间。没有任何配置可以完全缓解此漏洞;升级依赖是强制性的,并且必须使用 最终安全版本

升级清单

  • Next.js 15.x: 更新至 15.0.7+(不要停在 15.0.6)
  • Next.js 14: 更新至 14.2.35+
  • React: 更新至 19.0.3+(19.0.x 分支)或 19.1.4+

关键步骤: 必须 重新构建next build)并 重新部署 你的应用。易受攻击的代码已经被打包进服务器产物,单纯的重启是无济于事的。

事后回顾

React2Shell 以及它的“后代”漏洞将改变人们对 “全栈” 框架的讨论。我们为了开发者便利而牺牲了严格的关注点分离,结果自食其果。

这是否意味着 RSC 已经死亡?并非如此。但假设 Next.js 应用中的服务端代码“默认安全”的日子已经结束。

Back to Blog

相关文章

阅读更多 »