---
area: "tech-engineering"
category: technology
companies_orgs:
- Vercel
- Cloudflare
- FBI
date: '2025-12-09'
draft: true
guest: ''
insight: ''
layout: post.njk
media_books:
- 《让编程再次伟大#35》
- 《让编程再次伟大#49》
products_models:
- React
- Next.js
- Node.js
- gRPC
project: []
series: ''
source: https://www.youtube.com/watch?v=LSiYdiMGS4U
speaker: Peter Pang
status: evergreen
summary: 本文深入探讨了React Server Components（RSC）中一个满分10分的严重安全漏洞（CVE-2025-55182），该漏洞允许攻击者在服务器上执行任意代码。文章分析了漏洞的技术细节、对Next.js的影响，以及它如何阴差阳错地导致Cloudflare服务中断。同时，也批判了Vercel公司在SSR路线上的激进策略及其对后端安全原则的忽视，并强调了系统间依赖和安全防护的重要性。
tags:
- infrastructure
- risk
- security
- system
title: React满分漏洞深度解析：前端框架如何引发半个互联网瘫痪
---
### React满分漏洞：前端框架的致命安全隐患

在开发后端系统时，有一个基本原则至关重要：永远不要相信前端传来的任何数据。如果不遵守这一原则，结果可能会像**React**（一个用于构建用户界面的JavaScript库）一样，喜提满分10分的前端框架历史上最严重的安全漏洞。

2024年年底，万众期待的React 19发布，其中最重磅的更新自然是**React Server Components**（RSC: 一种全新的后端渲染机制）。对于这个当今世界上最受欢迎的前端框架而言，这是一个重要的里程碑，因为它意味着在**Vercel**（一家推动React Server Side Rendering路线的公司）的推动下，React彻底押注**Server Side Rendering**（SSR: 服务器端渲染）的技术路线。Vercel公司通过React大力发展SSR，为后端引流，然后用**Next.js**（一个基于React的SSR框架）做壳，为自家云服务引流，整个战略一气呵成，可以说是“光明正大的阴谋”。

早在今年年初，Next.js就出现过9.1分的严重漏洞。我在分析那个漏洞的《让编程再次伟大#35》中曾总结：“这些勾当的背后，鬼知道还有多少致命的漏洞。”现在我们知道了。

### 漏洞细节与潜在危害

2025年11月29日，来自新西兰的安全专家Lachlan Davidson向React团队提交了一份bug报告。他通过在**HTTP请求**（客户端向服务器发送的数据请求）里构建一个精心设计的**JSON**（JavaScript Object Notation: 一种轻量级的数据交换格式），成功让使用RSC的服务器执行了一段代码里完全没有的命令。如果只是借用服务器资源跑一下`console.log`，那倒也不是特别严重的事情，最多给个6分。让这个漏洞值10分的原因是，不管你在**payload**（攻击载荷，指恶意代码或数据）里写什么，只要它是正确的**Node.js**（基于Chrome V8引擎的JavaScript运行时环境）代码，React都会执行它。

例如，你可以调用Node.js的`child_process`模块，直接在服务器上执行**bash命令**（Unix-like系统中常用的命令行解释器命令）。如果你的后端是用**root**（Unix-like系统中的最高权限用户）之类的高权限账号部署的，那后果将更加严重，可以直接导致“一键扫荡”。如果你不想搞破坏，只是想要窃取一些机密，那也是轻而易举的事情。比如，通过`fs`模块读取任意一个系统文件，然后通过`https`模块把文件内容发送到自己的服务器。如果你懒得搭建自己的服务器，或者怕事情败露后被警察叔叔沿着网址摸过来，你也可以原地把窃取到的内容放进**Error**（编程中表示错误或异常的对象）里抛出，这样服务器就会用**HTTP 500报错**（服务器内部错误的状态码）把Error送回来。如果你想做得更隐蔽一些，免得忽然出现大量的Error log被人发现异常，你可以让HTTP请求正常返回，然后把机密塞到返回的**header**（HTTP请求或响应的元数据部分）里。

这个漏洞不仅严重，而且很容易复现，所以React团队陷入了一个两难的境地。如果偷偷地发布修复**补丁**（修复软件漏洞或错误的代码更新），大家不知道出大事了，就很难保证补丁的安装率。如果全世界通告漏洞的存在，那黑客肯定比谁的反应都快。

### React团队的应对与技术剖析

为了延缓黑客的速度，给大家争取足够的时间看到新闻并打好补丁，React团队在2025年12月3日仅发布了通告和补丁，没有解释任何技术细节。他们还特意在补丁中混入了将近1000行的其他代码，就是为了让人更难找到漏洞的源头并将其复现出来。但毕竟React是一个**开源项目**（源代码可以公开获取、使用、修改和分发的软件项目），一切都是明牌的，这种小伎俩是拖不了多久的。像我这种前端半桶水的程序员，也只是花了几个小时，把补丁涉及的那几个文件和它们的上游都看一遍，基本上就能捋清整个逻辑链条了。

简单来说，React的RSC模块在收到前端请求时，会进入一个**recursive parsing**（递归解析: 一种解析数据的方法，其中解析器会重复调用自身来处理嵌套结构）的逻辑。在这个过程中，**parser**（解析器: 将输入数据转换为程序可理解的结构）会生成一个类似**AST**（抽象语法树: Abstract Syntax Tree，源代码的抽象语法结构的树状表示）的数据结构。其中有一个叫做`parseModelString`的函数，负责识别这个数据结构。这里你能看到，它就是一个巨大的`switch case`（编程语言中根据不同条件执行不同代码块的控制结构），根据数据的第一个和第二个字符来判断它属于哪种数据。比如说，以`$Q`开头的就是**Map**（JavaScript中存储键值对的集合），`$W`开头的是**Set**（JavaScript中存储唯一值的集合），`$K`开头的是**FormData**（用于构建和发送HTML表单数据的接口）。

这里有一个比较特殊的情况是`$@`，它指的是一个**Promise**（JavaScript中用于处理异步操作的对象），也就是一个会异步执行的JavaScript命令。这种特殊数据就会被打包成一个独立的**Chunk**（数据块，通常指数据流中的一个独立单元），状态设置为`PENDING`（Promise的初始状态，表示异步操作正在进行），等待后续的数据流。数据流完整传输到位后，这个Chunk的**状态**（status）就会被更新为`RESOLVED_MODEL`（RSC中表示数据块已解析并可执行的状态），代表它可以执行了。在执行之前，需要先通过`parseModelString`函数对Chunk的内容再做一次parsing，把它从**binary**（二进制，计算机中以0和1表示数据的方式）转换成正确的JavaScript结构。比如说，`$A`是**ArrayBuffer**（JavaScript中表示固定长度的原始二进制数据缓冲区），`$S`是**Int16Array**（JavaScript中表示16位带符号整数数组的类型）。这里又有一个比较特殊的情况：`$B`指的是**Blob**（Binary Large Object，表示不可变的、原始数据的类文件对象）。因为Blob就是Blob，没有什么需要转换的，所以`$B`开头的数据会跳过**constructor**（构造函数，用于创建和初始化对象的特殊方法）的步骤直接执行。

现在我们再回头来看Lachlan Davidson提交的这个样本JSON，你就能看懂了。他先把这个payload转换成**string**（字符串，文本数据类型），然后再转换成**buffer**（缓冲区，用于临时存储数据的内存区域）。这样React的parser就会读到一个长得跟真实AST数据一模一样的东西。这里的`$@`告诉你它是一个要被执行的Promise，而这里的`status`等于`resolved_model`就让它绕开了不存在的`PENDING`状态，直接进入执行流程。这里的`$B`告诉你它是一个Blob，请你绕开constructor步骤直接执行。最后通过精准对应`$B`数据的处理代码，让parser通过它提供的`_prefix`和`_formData`这两个key，乖乖地把`_prefix`里的文本当做是Blob的内容，直接执行，也就是那行`console.log()`代码。

为什么这么简单的一个JSON就能伪装成功呢？因为React从头到尾都没有验证手上的这个AST数据是不是parser自己生成的。理论上你能够在请求内容里伪装任何东西，它都会傻傻地全盘接受。这也是让这个漏洞一举拿下10分满分，成为完美漏洞的点睛之笔。

### Vercel的责任与Next.js的特殊处理

在这个事件中，不存在背锅侠。因为整个React核心开发团队和幕后黑手Vercel公司都是罪魁祸首。Vercel一直在模糊React的前端和后端，这不是不小心的，而是故意的。因为他们需要让React使用者，那些前端开发团队，更容易接受这种极端的SSR路线。但本质上，这种跨网络的通讯就是一种**Remote Procedure Call**（RPC: 远程过程调用，一种允许程序调用另一个地址空间中的过程或函数的技术）。

RPC框架不是什么新鲜事物，它的安全机制也早就有了很成熟的设计准则。不管是早年的**SOAP**（Simple Object Access Protocol: 一种基于XML的RPC协议），还是现在最流行的**gRPC**（Google Remote Procedure Call: 一种高性能的RPC框架），都很遵守这些基本准则，比如说**schema**（模式，数据结构或数据格式的定义）的设计、**explicit的定义**（明确的定义）、防止**边界混淆**（指不同系统或模块之间界限不清，可能导致安全问题）的措施等等。然而Vercel这些人就是没有一点点后端开发的职业操守。从整个RSC的设计来看，你甚至可以说他们是把前端圈子的“**所见即所得**”（WYSIWYG: What You See Is What You Get，指在编辑时看到的效果与最终输出效果一致）作风带来了后端。就像在《让编程再次伟大#35》的结尾，我当时大胆地预言这不会是Next.js最后一次严重漏洞，在这里我也敢再次大胆预言，这也不会是React最后一次严重漏洞。

如果你有用到Next.js，务必认真听清楚这个章节的内容。在React上报漏洞（CVE-2025-55182）的同一天，Next.js也向**CVE**（Common Vulnerabilities and Exposures: 公开披露计算机安全漏洞的列表）提交了一个漏洞报告（CVE-2025-66478）。Next.js提交的这个报告，内容上和React提交的那个是完全一致的，整个报告都只在描述React的漏洞，没有别的。所以CVE拒收了这个漏洞报告，把它标记为`duplicate`（重复的）。这里就产生了一个理解误区，就是有不少人会误以为：“这次漏洞事件中Next.js本身没有出问题”，“如果我的项目里有安装React和Next.js这两个**依赖**（软件项目所需要的其他库或模块），我只需要更新React补丁就可以了。”注意！这是错误的！错误的！错误的！

因为Next.js不像其他框架那样纯粹把React当做一个依赖，它是把React的全部源代码都复制进自己的代码库里。如果你的项目同时安装了React和Next.js，那实际上你有两份React代码：一份是独立的React，另一份藏在Next.js的**bundle**（打包，将多个文件合并成一个或少数几个文件）里。所以如果你们团队有用到那种很高级的**自动化漏洞修复流程**（自动检测、下载和应用安全补丁的流程），可以自动监听和同步CVE，自动下载补丁，自动发布更新，那么你反而会漏掉Next.js的补丁，因为它被CVE拒绝掉了。所以务必记得手动把Next.js更新一下。虽然截止到目前，我还没有看到Next.js里的React代码和大家用的React有什么区别，但是司马昭之心路人皆知，这种做法显然就是为了以后做定制版本的React在铺路。我在这方面的第六感一向很准，大家走着瞧就是了。

### Cloudflare的连锁故障：一个意外的受害者

最后登场的是我们的老朋友**Cloudflare**（提供CDN、DDoS防护等服务的互联网基础设施公司）。这次它又给我们带来了让人拍案叫绝的精彩故障。本来作为一个**基建服务商**（提供基础设施服务的公司），React这么一个前端框架出漏洞应该跟他们是没啥关系的。但是根据我在Cloudflare内部的线人透露，他们高强度地使用React，而且水平很烂。果不其然，在React漏洞公布两天后，12月5日早上8点，Cloudflare又瘫痪了将近半个小时。只不过瘫痪原因并不是（React）漏洞被黑客发现导致的攻击，而是非常具有Cloudflare特色的连锁事故。

时间回到React漏洞公布当天，Cloudflare立马很自豪地宣布：自家的**防火墙服务WAF**（Web Application Firewall: 网络应用防火墙，用于保护Web应用免受攻击）上追加了对这次漏洞的防护，所有免费和付费的用户都可以自动享受。WAF对于HTTP请求的检测主要来自于他们的**proxy缓存**（代理服务器缓存，用于存储网页内容以加快访问速度）。目前这个缓存的上限是128KB。鉴于用上了RSC的人基本上也都是Next.js用户，而Next.js默认的**HTTP请求的主体**（HTTP请求中包含实际数据，如表单数据或JSON的部分）上限是1MB，所以Cloudflare决定把自家proxy的缓存上限也改成1MB，这样可以保证所有Next.js的流量都能够被WAF保护。只不过在发布这个改动时，他们发现了一个小问题：用来给WAF做测试的工具不支持1MB的数据体积，发布流程就被这个测试工具给卡住了。不过既然只是拿来做测试的，那关掉它也无所谓是吧？而事实证明，确实无所谓，只不过关掉它这个动作引发了另一个经典错误。

首先要知道的是，Cloudflare的所有模块都由统一的**规则引擎**（根据预设规则处理和路由请求的系统）来引流。一个请求是否经过某个模块，包括这个模块里负责测试的**子模块**（模块内部的更小功能单元），都是由一条条独立的规则所决定的。所以要关掉WAF的测试，要做的其实就是跳过WAF里指向测试模块的那条子规则。如果你有看《让编程再次伟大#49》就会知道，Cloudflare最近在迁移规则引擎，新旧两个版本的引擎在同时运行。上个月出故障的是用**Rust**（一种系统编程语言）重写的**FL2引擎**（Cloudflare新的规则引擎版本），而这次出问题的，则是基于**NGINX**（一款高性能的HTTP和反向代理服务器），用**Lua**（一种轻量级、可嵌入的脚本语言）写的**FL1引擎**（Cloudflare旧的规则引擎版本）。

画面上这段用Lua写的代码，主要负责总结子规则的执行结果。但因为WAF的测试子规则被跳过了，所以在总结它的结果时，这里的`rule_result.execute`是个空值。那它自然就没有一个叫做`results_index`的参数，那这就是经典的**null pointer exception**（空指针异常: 当程序试图访问一个空指针时发生的错误）了。这么低级的bug会出现在这里，是因为在FL1引擎的整个生命周期里，这段代码从未被使用过，也从未被测试过。如果不是因为这次的React满分漏洞，很有可能直到规则引擎的迁移结束，整个FL1引擎正式退隐江湖，也不会有人发现这个bug。

因为如果React的漏洞没有曝光，Cloudflare就没有机会蹭这个热度，就不会发布WAF对于Next.js的支持，就不会想要修改proxy的缓存大小，就不会发现WAF的测试工具不支持修改，就不会想要跳过这个测试，就不会去关掉规则引擎里的这条子规则，就不会触发这个bug，Cloudflare就不会挂掉。所以说，React的漏洞阴差阳错地瘫痪了半个互联网，妙哉，妙哉！

### 结语

无论是能力赶不上野心的React，还是又蠢又坏的Next.js，亦或是公式化草台班子的Cloudflare，我都已经没什么好总结的了。这半年就做了4次故障解读的我已经彻底麻木。往常的我还会去深究问题的根源，找出行业的本质缺陷，提出自己的改进意见。现在的我只想说一句：下一个傻子什么时候登场，我已经新建好文件夹了。