从 HTTP2Shell 的角度看兼顾安全性的语言选型
大家好,我是无能。
前些天,Brotli 解码器被发现存在漏洞,似乎被称为 Node2Shell、HTTP2Shell 等。
这里说的是 CVE-2026-111497,即解压恶意构造的 Brotli 压缩数据时会发生越界写入,可能导致进程崩溃、内存信息泄露,在某些条件下甚至可能导致任意代码执行。
也就是说,由于问题出在解码器一侧的实现,HTTP 客户端也需要更新。如果服务端启用了 Brotli,我想也需要进行更新处理,不过就影响程度而言,我认为 HTTP 客户端一侧受到的影响更大。
这次想聊的不是漏洞本身,而是在出现这类问题时,所采用的语言和库的组织方式,会让补丁分发的难易程度产生相当大的差异。
并不一定更新 OS 的库就万事大吉
例如,如果客户端使用的是 OS 共享库形式的 libbrotli,基本上可以通过各个 OS 的包管理器安装修复版本来应对。当然,前提是发行版已经提供了修复,而且正在运行的进程可能需要重启。
不过,这里说的是应用程序动态链接 OS 共享库的情况。
即使同样使用 Brotli,如果在构建时将库静态链接到可执行文件中,那么更新 OS 一侧的 libbrotli 也不会改变二进制文件的内容。你需要等待开发方发布修复版本,或者自行更新依赖并重新构建。
也就是说,重要的不只是“用什么语言编写”,而是“有问题的代码位于哪里,以及由谁的更新来替换它”。
Rust 的二进制分发很方便,但另一方面……
对于 Rust,普通的 Rust crate 通常会被嵌入可执行文件,方便以单个二进制文件的形式分发。实际上,我自己也觉得这种便利性非常不错。
另一方面,如果依赖的代码被发现存在漏洞,就会出现仅更新 OS 软件包无法修复的情况。要修复二进制文件中包含的依赖代码,原则上需要重新构建并重新分发应用程序。
就这次的 Brotli 而言,从 Rust 调用 C 实现的 brotli2 这类封装库,与用 Rust 实现的 brotli 这类库,情况也各不相同。而且,即使使用的是 C 实现,更新方式也会因动态链接共享库,还是连 C 代码也一起静态链接而有所不同。并不是说只要是用 Rust 编写的,就一定受这次 C 实现漏洞的影响。
说到底,Rust 的内存安全性与依赖代码是否易于更新,是两个不同的问题。
我认为 Rust 在库开发和性能关键部分非常有价值,但如果用于广泛分发的应用程序,就必须连同分发后的安全更新一起考虑。能够生成高速、独立自足的二进制文件是一项优势,而它的另一面,是将修复送达用户的责任也更多地落在了应用程序一方。
Node.js 可以通过更新运行时来处理,比较省心
Node.js 也受到了这次漏洞的影响。由于 Brotli 实现被嵌入 Node.js 本体,仅更新 OS 的 libbrotli 无法解决问题,需要更新到修复后的 Node.js 版本。
这次,Node.js 安全更新发布了 22.23.4、24.22.1、26.12.1 版本。
不过,对于一般的 Node.js 应用程序,只需更新 Node.js 运行时并重启应用程序,就能应用其本体中包含的 Brotli 修复。当然,如果 npm 包中包含了其他存在漏洞的实现,或者将 Node.js 本身打包进可执行文件,那就另当别论。
在这一点上,我觉得从某种意义上说,它比 Rust 的单文件二进制分发方式更容易运维。因为有些情况下,不必让每个用户重新构建大量应用程序,而是可以通过更新共用的运行时来应对。
不过,并不是只要使用 Node.js 就会触发这次漏洞。只有存在解压恶意 Brotli 数据的路径时,才会成为问题。印象中,在 HTTP 服务器上专门对收到的请求进行 Brotli 解压的实现并不多,但反过来,作为客户端访问外部的 fetch() 会自动解压经过 Brotli 压缩的响应,因此也不能只盯着服务器监听端就万事大吉。
以前我算是比较反对 Node.js 的,但看到最近标准 API 的完善程度,我的印象发生了相当大的变化。像 fetch()、Web Streams、测试运行器等,以前需要依赖外部包的功能,如今仅靠 Node.js 本体就能覆盖相当大一部分。
如果能减少依赖包,需要管理和更新的对象也会相应减少。与过去相比,作为编写应用程序的一种选择,Node.js 给我的感觉相当不错。
我还是觉得 Go 的设计很出色
在这方面,我觉得 Go 是一门在设计时充分考虑了应用程序开发的语言。
以 net/http 为代表的标准库很实用,在开发 HTTP 客户端或服务器时,往往无需引入大量外部依赖,仅靠标准包就能完成大部分实现。标准 HTTP 客户端自动解压的基本上是 gzip,也并非默认使用这次涉及的 Google Brotli C 解码器。
当然,Go 通常也会将依赖代码嵌入二进制文件,因此如果这些代码存在漏洞,同样需要重新构建和重新分发。并不是因为用了 Go,就只需更新 OS 的库;如果通过 cgo 使用外部库,也同样会遇到动态链接还是静态链接的问题。
即便如此,仅靠标准包就能完成的功能范围很广,而且容易将必要的依赖保持在较少的数量,我认为这不仅对可维护性,对安全性也有相当大的意义。
语言选型时需要考虑的不只是运行性能
这并不是说 Rust 不好,或者 Node.js、Go 就安全。说到底,这次漏洞存在于 Brotli 的 C 实现中,不能仅凭语言名称就判断是否受到影响。
不过,我认为在选择语言时,除了运行性能、内存占用和开发效率,还应该评估依赖库的更新最终由谁、以什么方式落实到用户端。
如果是共享库,就有可能通过 OS 软件包更新统一修复。另一方面,静态链接的二进制文件虽然可以连同依赖代码一起做到独立自足,但修复时需要逐个更新应用程序。像 Node.js 这样的运行时模式,虽然具有可以统一更新运行时本体的优势,但如果运行时本身出现漏洞,也会产生广泛影响。
归根结底,这是关于把什么嵌入到哪里、如何分发,以及负责维护到什么程度的设计问题。
就我个人而言,我希望积极将 Rust 用于库开发和需要性能的组件;但对于一般的应用程序,选择 Go 或 Node.js 的理由也相当充分。看到这次 HTTP2Shell 后,我再次有了这样的感受。