2026 年 10 月 21 日,PHP 和 Node.js 在同一天发布了紧急安全更新。两个漏洞都不在业务代码里,都出在运行时自身。安全社区给它们起了名字:PHP2Shell 和 Node2Shell。随后,Node.js 这个漏洞的根源被追溯到 Google Brotli 库,影响范围持续扩大,又有了一个更广的称谓:HTTP2Shell。
这篇文章梳理两个漏洞的机制、触发条件,以及开发者需要执行的操作。
PHP2Shell:补丁发布 19 分钟后出现利用
PHP 官方于 2026 年 10 月 21 日 14:00 UTC 发布修复,涉及 CVE-2026-109618 和 CVE-2026-109619,两个漏洞的 CVSS 评分均为 9.2。
CVE-2026-109618:OOM 处理路径的内存破坏
这个漏洞的根源在 PHP 处理内存耗尽的方式上。当脚本消耗内存超过 memory_limit,PHP 通过 longjmp() 提前终止请求。问题在于,部分代码在分配内存前没有预判这种提前退出,导致请求关闭阶段会访问未初始化或已失效的内存。
攻击者发送一个构造过的 multipart/form-data 请求,利用 session.upload_progress 机制在请求初始化阶段触发 OOM,就能制造内存破坏。更关键的是触发条件。安全研究者的测试表明,这条链在 Apache mod_php 的 prefork 模式下效果最好,因为 prefork 会复用进程内存,损坏的状态有机会被后续请求消费。PHP-FPM 在启动失败后通常会直接退出 worker 并丢弃状态,利用难度显著提高。
CVE-2026-109619:跨请求的释放后重用
第二个漏洞是跨请求的 UAF。PHP-FPM 或 mod_php 的工作进程会复用,前一个请求遗留的全局状态如果没有被彻底清理,后续请求就可能访问到指向已释放内存的悬空指针。攻击者先后发送两个请求,一个制造脏状态,一个触发利用。
利用现状
安全公司 watchTowr 在补丁发布前已经复现了其中一个漏洞。补丁公开后 19 分钟,他们观测到了针对这两个 CVE 的活跃利用尝试。亚马逊威胁情报通过蜜罐确认了攻击行为。CISA 于 10 月 26 日将两个漏洞列入已知被利用漏洞目录。
修复版本是 PHP 8.2.35、8.3.36、8.4.27、8.5.12。一个容易被忽略的细节:PHP 官方在 10 月 26 日重新打包了 Windows 版本,因为 Windows 构建捆绑的 Brotli 库正好受下面要说的 HTTP2Shell 漏洞影响。新包版本号带 -1 后缀,php -v 看不出来。
HTTP2Shell / Node2Shell:Brotli 解码器越界写入
Node.js 于 2026 年 10 月 21 日发布 22.23.4、24.22.1、26.12.1,共修复 10 个漏洞,最严重的是 CVE-2026-111497。
这个漏洞有多个名字:Node2Shell、HTTP2Shell、Broccoli2Shell。命名的演变本身说明了问题的扩散过程。最初在 Node.js 中被发现,所以叫 Node2Shell;后来研究人员确认根源在 Google Brotli 库,而 Brotli 被广泛用于 HTTP 压缩,Python、.NET、Envoy 等都在受影响范围内,于是有了 HTTP2Shell 这个更准确的称谓。它并非另一个新漏洞,而是同一个 CVE 在不同生态系统中的波及。
漏洞机制
问题出在 Brotli 解码器。一个精心构造的 Brotli 压缩流可以触发无效的块类型切换,解码器把这个错误状态误判为"需要更多输入",继续向一个 8 字节的内部缓冲区追加数据,最终越界写入并破坏相邻内存。
Imperva 的研究人员在官方 Node.js 24.22.0 Linux arm64 镜像上实现了代码执行。在修复后的 24.22.1 上,同样的数据被拒绝。
触发路径与受影响范围
关键在于"谁会解压 Brotli 数据"。一个纯 Node.js HTTP 服务器收到 Content-Encoding: br 的请求体时,不会自动解码。需要中间件或显式调用才会触发解码器。
Imperva 测试了两条路径:Express 5 配合 body-parser 2.x 的请求体解析(express.json() 默认解码 Brotli 请求体),以及 fetch() 读取 Brotli 压缩的 HTTP 响应。两条路径都能把恶意数据送到解码器。
影响范围持续扩大
CVE-2026-111497 的根源在 Google Brotli 库本身。受影响版本为 Brotli 1.0.3 至 1.0.9、1.1.0、1.2.0,修复版本是 1.0.10、1.1.1、1.2.1。
除 Node.js 外,以下组件也受影响:
-
Python :
brotli包(修复于 1.2.1)、brotlicffi(修复于 1.2.1.0)。urllib3/requests、httpx、aiohttp在安装 brotli 支持后会自动解压 Brotli 响应。 -
.NET :微软于 10 月 25 日发布紧急更新(8.0.33、9.0.22、10.0.14、11 RC3),ASP.NET Core 的解压缩功能、
HttpClient自动响应解压缩、Brotli API 在非默认配置下可能受影响的。 -
Bun:1.4.4 之前版本捆绑了受影响的 Brotli。
-
Envoy / Istio:配置了 Brotli 解压缩过滤器的部署受影响。Envoy 修复版本为 1.40.0、1.39.3、1.38.6、1.37.8。
一个容易被误解的点:仅更新系统层面的 Brotli 库不能保护自带副本的应用。Node.js、Bun、.NET 都捆绑了自己的 Brotli 实现,必须分别升级。
Node.js 20 及更早版本已经 EOL,不会收到补丁。仍在运行 Node 20 的系统需要优先迁移。
利用现状
公开的 PoC 已经出现在 GitHub 上。据 Broccoli2Shell.com 的记录,PoC 在刻意削弱的安全构建上可达代码执行,在标准构建上仅导致崩溃和有限的内存读取。截至搜索结果记录的时间,尚无确认的野外恶意利用报告。但这不改变升级的紧迫性。
为什么业务代码防不住
这两个漏洞的共同点是:触发点不在开发者编写的逻辑里。
一个 PHP 开发者可能从未用过 unserialize、eval、system,SQL 全走预处理,文件包含全走白名单。这些习惯有价值,但在 PHP2Shell 面前不起作用。漏洞在解释器处理请求生命周期的 C 代码里,业务逻辑还没执行,内存已经被破坏。
Node.js 同理。你只是用了框架默认的请求体解析,或者只是调用了 fetch(),解码器就在底层被触发了。你写的中间件逻辑再严谨,也无法拦截一个在中间件之前就已经发生的越界写入。
这类漏洞的攻击面是运行时提供的基础能力,不是开发者主动编写的功能。防御手段只有跟进官方安全版本。
需要执行的操作
PHP 项目 :运行 php -v,低于 8.2.35、8.3.36、8.4.27、8.5.12 的立即升级,并重启 PHP-FPM 或 Apache。Windows 版本还需要确认是否使用了带 -1 后缀的重打包版本。
Node.js 项目 :检查 node -v,低于 22.23.4、24.22.1、26.12.1 的立即升级。容器化部署必须用新版本基础镜像重新构建并重新部署。Node.js 官方公告中没有提供运行时层面的缓解措施,补丁是唯一的修复手段。
Python 项目 :检查 brotli 是否低于 1.2.1,brotlicffi 是否低于 1.2.1.0。如果无法立即升级,卸载 brotli 相关包并移除 httpx[brotli] 等可选依赖可以降低风险,但这会牺牲压缩性能。
.NET 项目 :检查是否使用了 Brotli 解压缩功能(BrotliStream、HttpClient 自动解压缩、ASP.NET Core 请求解压缩)。这些功能默认未启用,但一旦启用就需要升级到对应补丁版本。
缓解措施 :如果无法立即打补丁,可以在代理、负载均衡器或 WAF 层面拒绝 Content-Encoding: br 的入站请求体。但这不能防护应用主动请求并解压恶意 HTTP 响应的场景。
小结
PHP2Shell 和 HTTP2Shell/Node2Shell 在同一天爆发。它们的共同教训是:现代 Web 应用依赖的解释器、运行时、内置压缩库由大量底层 C 代码组成,这些代码的缺陷被利用时,上层应用的安全措施无法拦截。
HTTP2Shell 的命名演变还说明另一个问题:一个库的漏洞可以沿着依赖链扩散到整个技术栈。Brotli 是 Google 维护的压缩库,被 Node.js、Python、.NET、Envoy 各自捆绑或引用,上游一个越界写入,下游需要多个团队分别发布补丁、分别升级、分别重启。作为开发者,能做的只有把运行时版本管理纳入日常运维:订阅安全公告,定期检查生产环境版本号,在 CI/CD 中加入版本合规检查。