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次故障解读的我已经彻底麻木。往常的我还会去深究问题的根源,找出行业的本质缺陷,提出自己的改进意见。现在的我只想说一句:下一个傻子什么时候登场,我已经新建好文件夹了。
📌 文中提及的人物和组织
公司/组织: Vercel, Cloudflare, FBI
产品/模型: React, Next.js, Node.js, gRPC
媒体/书籍: 《让编程再次伟大#35》, 《让编程再次伟大#49》