2026年11月2日星期一
漏洞

Brotli解码器RCE波及Node.js、Bun、Python与Envoy

CVE-2026-111497源于Google Brotli解码器中的同一个C语言缺陷,已复制到所有集成该组件的运行时。本文梳理已证实与已宣称的内容,以及修补方法。

一个破损的组件将缺陷复制到众多相连机械部件之间的插图。
Google C解码器中的一个缺陷,被复制到每个引入该代码的运行时中。

Google Brotli解码器中的一个内存破坏漏洞CVE-2026-111497,与其说是常规的库漏洞,不如说更像一场分布式的隐藏关卡BOSS战。问题出在参考C解码器中,这意味着每个引入该代码的项目都携带着自己的副本:Node.js、Bun、Python的Brotli和brotlicffi包、启用Brotli解压时的Envoy和Istio,以及大量绑定库、代理和命令行工具。The Hacker News报道称,Imperva红队已在一个未打补丁的Node.js构建上演示了实际的代码执行,但仅限该构建,目前尚无野外利用。修复它不是打一个补丁那么简单,而是要在传递性依赖中进行一场大海捞针式的排查。

该漏洞于10月21日披露,当日Node.js和Google发布了修复,其他软件的补丁则陆续按产品发布。OpenAI的Codex Security团队(具体为Node.js致谢名单中的Khai Tran和Sylvie Mayer)将其报告为Brotli中的未认证远程代码执行漏洞,并且可双向利用:恶意客户端攻击解压上传内容的服务器,或恶意服务器攻击获取压缩响应的客户端。Google在CVSS 4.0下给出9.2分(严重);Node.js称其为高危;Red Hat给出8.1分。评分上的这种差异,就是第一个提示:可利用性的故事比标题所暗示的更为复杂。

该漏洞的实际工作原理

broccoli2shell.com上的技术分析条理清晰地拆解了这一原语,它是一个经典案例。Brotli流被分割成多个块,解码器在读取时会在块类型之间切换。c/dec/decode.c中的函数DecodeBlockTypeAndLength()返回的是一个简单的布尔值,因此调用方无法区分格式错误的流与仅仅需要更多输入的流。在快速路径上,解码器完全忽略了该返回值。

攻击者构造的流请求了一次无法完成的块切换。解码器将其误判为需要更多输入,于是持续向一个8字节的内部缓冲区追加攻击者可控的字节,并写越界。整条链路就是:未检查的返回值(CWE-252)导致越界写入(CWE-787),而覆写会破坏相邻的解码器状态。此后,根据堆布局和宿主程序的不同,结果可能是崩溃、内存泄漏,或者在经过足够的堆布局操控后实现代码执行。

// Upstream fix: google/brotli commit 0ed4268 (plus 50a9b70, b418fa8, da98063)
// Before: boolean return — malformed stream indistinguishable from needing more input
// After:  hard format error returned to the caller
return BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH;  /* -17 */

更令人不安的是:修复提交在2月27日就已进入Brotli源码树。当时没有人意识到这是安全修复,而Brotli也整整八个月没有发布新版本。OpenAI自己的说明称,根本原因“实际上已在2月的Brotli源代码中修复,但当时并不知道它会引发安全漏洞,且之后也没有新的Brotli版本发布。”补丁早已存在,只是躺在代码仓库里,而所有下游使用者仍在发布带有漏洞解码器的版本。如果你需要理由来说明跟踪内嵌依赖来源的必要性,这件事就是最好的例子。

已证实、已宣称与未知:请分开看待

这里讲究审慎,因为这件事的信噪比相当糟糕。可以分为三类:

  • 已证实:越界写入存在于Brotli解码器1.0.3至1.2.0版本中,已在1.0.10、1.1.1和1.2.1中修复。Imperva红队(项目名为Node2Shell)在Linux ARM64平台的未修改Node.js 24.22.0构建上实现了代码执行,攻击通过获取的Content-Encoding: br响应以及Express请求体两种方式投递。该利用尝试了好几次才成功。
  • 已宣称但未经演示:OpenAI描述了一种双向、未认证的RCE,影响Node.js、Bun和Envoy,包括恶意服务器攻击客户端的场景(可参考curl --compressed)。考虑到该原语,这一说法是合理的,但除了那个Node.js构建之外,尚无人展示过任何其他软件上的代码执行。该利用也未公开。
  • 未知:是否会出现真实世界的利用。CISA在10月23日的评估中将利用情况记为“无”,自动化记为“否”。Red Hat指出,目前没有可靠的公开概念验证可用于执行代码。跨构建、分配器和架构的堆布局操控可靠性仍是未解问题,ARM64 Linux被证明是可行的,仅此而已。

为什么这种区分很重要?因为补丁优先级应当依据已演示的可利用性乘以暴露面,而不是披露中最吓人的那句话。已证实的情形是可通过HTTP访问的服务端解码器。双向RCE的说法是报告者对该原语能力上限的评估,而非已记录的能力。两者都支持尽快打补丁:一个已公开、且具有已知8字节堆溢出原语的漏洞,对漏洞利用开发者来说是块肥肉。但在概念验证尚不存在的情况下,没有人应该宣布事件已经结束。

你的暴露面比依赖清单显示的更广

暴露只需要一个条件:你的程序解码了他人可控的Brotli数据。麻烦之处在于,有太多路径无需出现在你的代码中就能做到这一点。

在Node.js中,内置的fetch()会透明地解码Content-Encoding: br响应,而Express 5 / body-parser 2会解压Brotli请求体,因此webhook、链接预览抓取器、爬虫以及“从URL导入”功能都是攻击面。注意broccoli2shell.com上的细节说明:Express和body-parser本身并不包含该漏洞,它们只是通往漏洞的路径。Node自带一份Brotli副本,因此系统中libbrotli的版本在这里并不相关,只有运行时版本才重要。单纯的node:http服务器本身不会解码请求体。

Python的情况更隐蔽。只要安装了Brotli或brotlicffi包,urllib3(进而是requests)、aiohttp、带brotli扩展的httpx,以及Scrapy等爬虫,都会自动解码Brotli响应。即使源代码中从未出现“brotli”这个字符串,你的应用也可能完全暴露。这是一种传递性依赖的幽灵:漏洞不在你编写的代码中,甚至不在你导入的库中,而在你的HTTP客户端悄悄加载的东西里。

# Don't trust the scanner — check by hand
pip show Brotli brotlicffi
# Vulnerable: Brotli <= 1.2.0, brotlicffi <= 1.2.0.x
# Fixed:      Brotli 1.2.1, brotlicffi 1.2.1.0

而你目前还真不能完全信任扫描器。CVE记录指向的是Google的Brotli代码仓库,而非PyPI包;在披露时,Dependabot所读取的GitHub公告并未列出任何受影响的包。两个Python包对应的PYSEC-2026-4322和PYSEC-2026-4323直到10月23日才出现。如果这件事对防御者有什么教训,那就是:依赖CVE的工具系统性地低估了内嵌C代码的数量。我们在LiteLLM供应链事件中也见过类似问题的较轻版本:一个带后门的包躲过了生态系统检查。公告图谱总是落后于实际制品图谱。

隐藏的嵌套依赖位于软件栈可见表面之下的插图。
易受攻击的解码器隐藏在你的清单从未列出的传递性依赖中。

Envoy、Istio以及运行时之外的波及范围

仅当配置了Brotli解压(例如通过过滤器)时,Envoy 1.31.0–1.31.1、1.30.0–1.30.5和1.29.0–1.29.8版本才受影响。修复版本为1.31.2、1.30.6和1.29.9。Istio遵循相同的条件逻辑。如果解压功能未开启,该漏洞就无法触发,这至少给平台团队提供了一个清晰的排查问题。

除了上述具名软件,任何发布或链接了Brotli 1.0.3–1.2.0的程序,即便在公告提及之前也可能受影响:系统库(libbrotlidec,以libbrotli1及相关名称打包)、Java JNI封装、PHP和Ruby扩展、Go cgo绑定,以及归档工具。用其他语言重写的解码器是安全的:Java、JavaScript和C#移植版无法触及该漏洞;Rust的brotli-decompressor会捕获该条件并报错退出;Go移植版(andybalholm/brotli)存在相同的逻辑错误,但只会崩溃,已于10月23日修复。代码执行只属于C解码器的特权。选择语言作为利用缓解手段,再次证明了其价值。

缓解措施:哪些有效,哪些无效

先说无效的做法。从客户端的Accept-Encoding请求头中去掉br并没有用处,因为恶意服务器无论你请求什么都可以发送Brotli。WAF规则也有硬性上限:Vercel在披露当天发布了防火墙规则,但表示这些规则不覆盖“函数从其他服务器获取的响应”。对于出站获取这一路径,只有升级运行时或库才能堵住漏洞。而对Node.js而言,目前尚未发布任何变通方案,升级运行时是唯一的修复途径。

根据OpenAI的披露页面以及安全公司Aikido的说明,在无法立即打补丁的情况下,以下做法有效:

  • 在边界处直接拒绝带有Content-Encoding: br的入站请求,早于任何代理或解析器接触请求体。
  • 在HTTP客户端中禁用自动Brotli解码,并在解码前拒绝Brotli响应。
  • 在Python中彻底卸载Brotli和brotlicffi,使客户端回退到gzip和deflate。
  • 打补丁后重启服务并重建容器镜像。正在运行的进程会在内存中保留旧解码器直至重启,而镜像在基于已修补基础镜像重建之前都会保留旧解码器。

版本矩阵简述如下:Node.js 22.23.4、24.22.1和26.12.1已修复(20.x及更早版本已停止维护,应迁移);Bun 1.4.4已修复;Python Brotli 1.2.1和brotlicffi 1.2.1.0已修复。在发行版方面,Debian于10月23日为trixie的brotli包打了补丁(1.1.0-2+deb13u1),而截至10月22日,Amazon Linux 2023仍将brotli和Node.js 24列为待处理。预计会有这种滞后,这正是为什么“我们已打补丁”必须指的是正在运行的进程,而不是包管理器的待办清单。

退一步看,这正是生态系统一再重新学习的教训,只是难度已升级到BOSS战级别:内嵌C代码会放大风险。解码器中一个未检查的布尔值,被复制到十几个运行时中,修复成本就要按每个使用者、每次重启、每次重建来支付。值得肯定的是:Khai Tran和Sylvie Mayer发现并报告了该漏洞,Node.js维护者Matteo Collina和Antoine du Hamel迅速发布了修复,而broccoli2shell.com的分析文章是目前最清晰的技术参考。目前尚无野外利用。在有人发布改变这一判断的概念验证之前,请尽快打补丁。

来源

本文参考了以下报道与公告: