当前位置:首页 > 编程开发 > 正文

HTTP2Shell 剖析:一个 Brotli 解压越界写,为什么波及 Node、Python、Envoy 一整圈

约 5606 字 · 预计阅读 15 分钟
一句话总结:CVE-2026-111497 是 Google Brotli C 解码器里的一个堆越界写,CVSS 4.0 给了 9.2 分。它的特殊之处在于位置太底层:Node.js、Bun、Python 的 requests/httpx、Envoy、Istio、cuRL 都会顺带把这段代码打进自己的产物,所以同一个 bug 在三天内沿着各个语言栈炸了一圈。

十月中旬先有一个名字叫 Node2Shell,指的是 Node.js 打包的 Brotli 库出问题。两天后 Python 那边补了同一个洞,接着 Envoy、Istio、Bun 陆续发版,社区才意识到这不是某个运行时的锅。于是有了 HTTP2Shell 这个叫法。

顺带纠正一个容易误会的点:这个名字跟 HTTP/2 协议没关系。取这个名字是因为 HTTP 内容编码(Content-Encoding: br)是最常见的到达路径,跟 h2 还是 h1.1 无关。我第一次看到的时候也以为要做协议层排查,白翻了一会儿 RFC。

先确认你在不在影响面里

漏洞本身很简单:Brotli 1.0.3 到 1.2.0 版本受影响。麻烦的是「受影响」不等于「你 pip list 能看到」,因为大量项目是把 Brotli 源码 vendored 进去的。

组件 受影响版本 修复版本
Google Brotli / libbrotli1.0.3 – 1.2.01.2.1(老线 1.0.10 / 1.1.1)
Node.js22.x / 24.x / 26.x22.23.4 / 24.22.1 / 26.12.1
Bun1.0.21 – 1.4.31.4.4
PyPI Brotli1.2.0 及以前1.2.1
PyPI brotlicffi1.2.0.x1.2.1.0
Envoy1.36.11 / 1.37.7 / 1.38.5 / 1.39.21.36.12 / 1.37.8 / 1.38.6 / 1.39.3 / 1.40.0
Istio1.29.0–1.29.8 / 1.30.0–1.30.5 / 1.31.0–1.31.11.29.9 / 1.30.6 / 1.31.2
Go andybalholm/brotliv1.2.7 之前v1.2.7

Wiz 给了一个数字:91% 的云环境里有可被这个漏洞影响的资源,82% 的云环境存在公网暴露的 Node.js 实例。这个比例听起来夸张,但想想容器基础镜像、API 网关、服务网格里塞了多少 Brotli,就不难理解。

还有一个更隐蔽的事实:你的代码一行都没提 Brotli,也可能中招。Python 侧只要环境里装了 Brotli 或 brotlicffi,urllib3(也就是 requests)、带 brotli extra 的 httpx、aiohttp 就会自动声明 Accept-Encoding: br 并解码响应。攻击路径是你去请求一个攻击者控制的地址,对面回一段精心构造的压缩流。

原理:一个布尔返回值引发的越界写

漏洞点在上游 c/dec/decode.c 的 DecodeBlockTypeAndLength(),这个函数负责处理 Brotli 流里的「块类型切换」,也就是切换 Huffman 树的位置。

问题在于返回值的语义被压扁了。这个函数返回一个普通 bool,BROTLI_FALSE 同时表示两件完全不同的事:

调用方没法区分两者。更糟的是快路径上干脆把这个返回值丢掉了。

攻击的构造思路就顺着这个模糊地带:先声明单一 block type,把这个 block 的长度耗尽,逼出一次注定失败的切换。解码器于是走到两条错误路径之一。第一条是「流已经完整了但还说要更多输入」,这一条顶多让解析卡住。第二条则是带着旧 block 状态继续解码,而这时缓冲区的位置已经不对了,写指针越过分配边界,相邻的 decoder state 被覆盖。

CVE 记录同时打了两个 CWE:CWE-252(未检查返回值)和 CWE-787(越界写)。这个组合恰好是上面这条链路的精确描述。

上游修复是 comMIT 0ed4268,把函数改成返回错误码,并新增了一个专门的错误 BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH(-17),让安全路径和快路径都在格式错误时停下来。有团队对比了 1.2.0 和 1.2.1 的源码包,发现两者只差版本号和这一个 commit。

AddressSanitizer 的对照测试很直观:喂一段构造好的约 23MB 流,1.2.0 会把数据全吃完还继续要输入(整块喂和逐字节喂都一样),1.2.1 每次都直接返回 -17。

图 1:上下两条攻击路径都通到同一个解码器,下半部分是根因的四步

双向可达是这个漏洞值得单独写一篇的原因。恶意客户端能打解压请求体的服务端,恶意服务端也能打解压响应的客户端。webhook、链接预览、爬虫、URL 导入、会浏览网页的 AI Agent,全在范围内。而入向 WAF 规则通常只管请求,管不到你去抓别人响应的那一段。

自查:三条命令加两处人工确认

优先级按「能扫出来的先扫」排。

第一步,看运行时版本。 这是最容易查也最容易漏的,因为很多人只看应用依赖不看运行时本身。

node -v    # 需要 >= 22.23.4 / 24.22.1 / 26.12.1
bun -v     # 需要 >= 1.4.4

第二步,查 Python 环境里的两个包。 注意要查所有环境和所有镜像,包括 CI 里临时建的那个。

pip show Brotli brotlicffi
pipdeptree -r -p Brotli      # 谁把它拉进来的
grep -rn "Brotli\|brotlicffi" requirements*.txt poetry.lock uv.lock

第三步,查网关和服务网格。 Envoy / Istio 的版本号不会出现在应用仓库里,得去基础设施配置看。

envoy --veRSIon
istioctl version

剩下两处只能人工确认,这两处恰恰是最容易出事的地方:

一是容器基础镜像。node:24-slim、python:3.12-slim 这类镜像里的 Brotli 是编译进去的,manifest 扫描(SCA)看到的是「没有依赖声明」,会报干净。Wiz 的调查里明确提到,绝大多数有漏洞的副本都是 vendored 进运行时、wheel 和静态二进制里的,所以基于 manifest 的扫描全部漏报。

二是能不能禁掉 br。如果升级排期长,临时缓解是:边缘层拒收 Content-Encoding: br 的请求体,同时关掉服务的响应自动解压。前者防入向,后者防出向,只做一半等于没做。

坑清单

坑一:pip-audit / Dependabot 报你干净,其实是漏洞库没映射上。 到 10 月 23 日为止,CVE 还没映射到 PyPI 的 Brotli 和 brotlicffi 包,所以这几个工具都不会标红。别把这个当结论,手动 pip show 一遍。

坑二:只升级了应用依赖,没重建镜像。 静态链接和 vendored 的副本不会因为 pip install -U 就更新。改完 lockfile 必须 rebuild 镜像再 restart,然后在跑起来的容器里再验一次版本,不要在构建机上验。

坑三:以为 Apache 的 mod_brotli 也中招。 它只负责压缩输出,不解码请求体,不在影响面上。这类「名字里有 brotli 但路径不同」的组件要逐个判断,不要按关键词一刀切。

坑四:修了服务端没修客户端。 出向路径同样能打。服务 A 调服务 B,B 挂了没关系,但如果 A 会去抓任意外部 URL(导入、预览、抓 RSS),A 就是那个暴露面。

坑五:把这次当协议层问题排查。 名字带 HTTP2,但跟 HTTP/2 无关。查 TLS、查 h2 帧格式、查 HPACK 全是浪费时间。

边界与后续

这篇只讲原理和自查,不给利用代码。原因不是保守:这个洞的真实利用可靠性在研究人员手里都「因环境而异」,Imperva 在实验室跑通了 Node.js 24.22.0 arm64 的原生构建,但复现率不稳定。对着别人的线上系统试,收益和风险都不划算。

要往深了走,三个方向:读一遍 0ed4268 这个 commit 的 diff(只有几十行,是很好的「错误处理设计」教材);了解一下 Decompression Bomb 和「压缩比与信任边界」的关系,这类洞最近两年明显变多;以及在自己环境里把上面的自查跑一遍,把这个清单固化成 CI 检查。站内另一条线是客户端方向的反调试识别,排查顺序的思路可以互相照搬。

目前 CISA 的 SSVC 评估是「未发现野外利用,未进 KEV」,所以不用恐慌,但升级别拖。这类底层库的洞一旦有公开 PoC,扫描器全网扫一遍只是几天的事。

参考资料

目录导航
  • 先确认你在不在影响面里
  • 原理:一个布尔返回值引发的越界写
  • 自查:三条命令加两处人工确认
  • 坑清单
  • 边界与后续
    • 参考资料
  • 相关文章:

    发表评论

    ◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。