Featured image of post 活久见 !PHP 爆出漏洞,同时还有 GO 和 Node

活久见 !PHP 爆出漏洞,同时还有 GO 和 Node

PHP 10 月 21 日发了 8.5.12、8.4.27、8.3.36、8.2.35 四个版本,修掉两个不用登录就能触发的 RCE,CVSS 9.2,mod_php 上应用代码还没跑就可能被打穿。Go 1.26.10 / 1.27.3 补了份证书黑名单,不重新编译等于没修。Node 补十个 CVE,Brotli 那条要自己解压外部数据才够得着。

活久见 !PHP 爆出漏洞,同时还有 GO 和 Node

这年头没想到像 PHP 这种古法编程语言,还能爆出这种级别的洞。不巧的是 10 月 21 日晚上 22 点,PHP 安全团队真的发了公告,8.5.12、8.4.27、8.3.36、8.2.35 四个版本一起修两个未认证远程代码执行,CVSS v4.0 都是 9.2,社区给它们起名字叫 php2shell。

据我了解,PHP 写的项目里全球用户最多的应该就是 WordPress,出海做独立站那批尤其扎堆在 WordPress + WooCommerce 上,支付、订单、会员数据全在同一个进程里。按 W3Techs 的统计,服务端语言已知的网站到现在还有接近七成跑着 PHP。再加上服务器上挂着的那些老项目,5.x、7.x 挪不动,安全更新早就断了。

无独有偶,同一周里 Go 和 Node 也各发了一版安全更新,以前一个漏洞修起来都够呛,现在三连发,真的不让码农活命了哈哈哈。

PHP

PHP 官方把这两个 CVE 拆开写,成因不一样。

CVE-2026-109618 走的是内存耗尽这条路。PHP 在 OOM 时会从当前任务里跳出来,这时候请求内存可能只初始化了一半,后面的清理逻辑把这份残缺数据当成正常数据用,就出事了。攻击者不用找什么特殊接口,构造一个 multipart 请求、在正常大小限制内把内存逼爆就行。官方原话是这个过程 “happens entirely before any application-specific code begins execution”,在 Apache mod_php 上已经在默认限制下验证过未认证利用。

CVE-2026-109619 是跨请求的陈旧状态。worker 进程会跨请求保留一部分全局状态,某些操作访问到了上一个请求没重置干净的数据,形成堆上的 use-after-free,能拿内存泄露也能拿 RCE。这个需要两个请求落到同一个 worker 上。官方确认的路径有两条,PHP 8.5 的 fatal error backtrace 和 session 上传进度。

两者的共同点在于请求异常结束时,PHP 把内存留在了损坏状态。这两个洞是 Calif、Wiz 和 OpenAI 的安全研究员一起找出来的。

官方对部署方式的判断很具体,不是一刀切。Apache mod_php 被列为风险最高,有演示证明未认证请求可以在任何应用代码运行之前触发。PHP-FPM 在 109619 的 session 上传进度路径上有确认的利用,109618 那边官方只说 susceptible,没给完整的 FPM 利用。FrankenPHP 和 LSAPI 是基于源码分析的怀疑对象。

四个分支各自对应一个新版本,对上就行。

1
2
3
4
PHP 8.5.x  →  8.5.12
PHP 8.4.x  →  8.4.27
PHP 8.3.x  →  8.3.36
PHP 8.2.x  →  8.2.35

官方只给受支持分支发包。8.1 及更早没有官方更新,但这次给了 PHP 7.4 / 8.0 / 8.1 的最小化源码回移分支,走发行版或者自己编的话能用上。5.x 和 7.0 到 7.3 是真没有,要么找 LTS 厂商要,要么从公网摘下来。另外 PHP 8.2 分支今年 12 月 31 日 EOL,8.2.35 是这个分支最后几个计划内的安全包之一。

配置救不了这两个洞。PHP 8.6 的 release manager Joe Ferguson 说得更直白,「ini settings won’t save you」。官方 blog 里给的那些缓解措施(memory_limit=-1、post_max_size 调小、session.upload_progress.enabled=0、fatal_error_backtraces=0)只盖住部分已知路径,属于争取时间的操作,不是安全状态。

这次之所以这么紧,是因为补丁在发布前就被泄了。10 月 20 日凌晨 Hacker News 上有人发帖爆料,帖子很快被删,但截图已经在社交网络上转了一圈。watchTowr 随后确认漏洞是真的,并且复现了 109618。PHP 原计划周四发版,因为细节已经流出,直接挪到了周三。补丁正式出来之前,Cloudflare 的 CTO Dane Knecht 已经在推上说 “seeing early attacks emerging”,同时给所有客户(含免费版)上了 WAF 规则,但也明确讲这只是 stopgap,而且只覆盖走 Cloudflare 的流量。

Go

CVE-2026-111943,Go 漏洞库里编号 GO-2026-6809,10 月 21 日凌晨公开(UTC 00:52,北京时间早上八点五十多)。修复在 Go 1.26.10 和 1.27.3,走的是 Go 的 URGENT 发布通道,这两个版本 10 月 20 日周二就出去了,所以严格说补丁比 CVE 记录早一天。

补丁内容就是一份 denylist。crypto/x509 里的 Certificate.Verify 碰到包含这些中间证书的链,直接返回错误。

背后是 eMudhra 那件事。八个中间 CA 的 P-384 私钥在 2025 年 4 月生成时随机性不够,值小于 256,也就是暴力试 256 次就能把私钥推出来。事后公开出来的一个键,值是 4。研究员 Wayne 在 10 月 17 日周六报给 Chrome Root Program,自证方式是拿每个密钥各签一条 “We have a problem here – Wayne”。

浏览器的反应是小时级的。Chrome 用 1 小时 40 分推了 CRLSet 拦住六个 TLS 中间证书,Firefox 三小时内上了 OneCRL。后面几天是各家运行时和信任库跟在后面收尾。

时间谁动作
10/19苹果中间证书加黑名单
10/20微软平台侧阻断在这天生效
10/20Go1.26.10 / 1.27.3 把八个证书编进 denylist
10/21Node.js22.23.4 / 24.22.1 / 26.12.1 拒掉含被拉黑公钥的链
10/21印度 CERT-In发公告,建议改信任设置、换证书
10/27Chrome把 emSign ECC Root CA - G3 整个根摘掉

最后一行值得单独记一下,那天之后还在用这条链的正常站点也会连不上。另外 Python 的 certifi、Rust 的 webpki-roots、PHP 的 composer/ca-bundle 这几天也都改了包,思路都一样,把自己那份根证书和黑名单更新掉。

好消息是目前没看到滥用痕迹。Chrome 说在 block 之前,CT 日志里没有任何用这六个中间证书签出来的证书,看起来密钥虽然能推出来,但还没被用上。

对 Go 程序来说,这里有个容易漏的点,这道 denylist 编在工具链里。Go 在大多数平台直接读系统根证书,系统信任库更新之后链自然就断了。但在容器、精简镜像、或者自带 root store 的场景里,系统那层不一定跟得上,所以 Go 选择自己也拦一道。问题在于 go.mod 里没有版本号可以升,go get -u 也解决不了,只能换 toolchain 重新编译、重新发版。

要不要现在就动,问两个问题就够了。你的服务会主动验证 TLS 吗(出站 HTTPS、webhook、gRPC、调第三方 API)?它跑的系统信任库能及时更新吗?两个都是「是」,优先级可以往后放;有一条不是,就别等下一个发版周期。

Node

Node 生态在 10 月 15 日已经有一波了,不过那波在 undici 里,编号 CVE-2026-87018。TLS 1.2 服务端在连接建立后重新协商、换一张主机名不匹配但 CA 仍然信任的证书,undici 的 socket 错误处理里有个断言认为这个错误码不可能出现,结果异常直接把进程带走。修在 6.29.1 / 7.30.1 / 8.12.2。同期还有一个 WebSocket 客户端的 HTTP/2 握手 DoS。

10 月 21 日这波是 Node 本体,22.23.4、24.22.1、26.12.1,十个 CVE,四个高危。

四个高危里三个是校验类问题。dns.resolve 碰到 NUL 字符会把域名截断(CVE-2026-105482);IP 写成全角数字时 TLS 主机名验证能被绕过(CVE-2026-105483,只影响 26 和 24);主机名能被宽松解析成 IP 地址时同样能绕过(CVE-2026-106069)。这三个要不要紧,取决于你有没有把用户输入直接送进 TLS 校验或者 DNS 解析,属于要查但不一定有。

真正值得单独看的是 CVE-2026-111497,Brotli 解码器越界写,可以导致远程代码执行,影响 22 / 24 / 26 三条线。这条的可达性有前提,Node 的 http server 不会自动解压请求体,所以不是所有服务都暴露。踩得到的是两类,一是代码里直接调 zlib.brotliDecompress,二是用 fetch / undici 拉外部资源、让它自动解 content-encoding: br 的响应。前者 grep 一下就知道,后者看你有没有在信外部 URL 的内容。顺手可以用 zlib.brotliDecompressSync 在本地复现一遍调用链,确认自己走的是哪条。

1
2
# 先看有没有直接解压 Brotli
grep -rn "brotliDecompress" --include="*.js" --include="*.mjs" --include="*.ts" .

另外一条容易被划过去的是 CVE-2026-106074。crypto.randomUUID() 和 randomInt() 的熵缓存会被写进 user-land startup snapshot 和 SEA,导致每个进程重复同样的随机值。如果你的服务用 SEA 打包、又拿 randomUUID 生成 session id 或者 token,那等于所有实例共用一套随机序列。CVSS 标的是中危,实际后果比标价重。

这版还顺手升了 c-ares、Undici、OpenSSL(24 / 22)、SQLite(22),以及把 eMudhra 那条链加入阻断,也就是拒掉含被拉黑公钥的证书链。

先动谁

PHP 先动,它没有前置条件。 先把版本号打出来。

1
php -v

对照 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35。低于这条线、发行版有包的直接升;还在 5.x / 7.x / 8.1 的,官方没包,一周内要么把回移包搞到手,要么先摘公网。Cloudflare 那套 WAF 规则可以先挂着,但它只盖已知向量,只对走 Cloudflare 的流量生效,别把它当解决方案。另外提醒一句,共享主机和面板托管的场景,你改不了 PHP 本体,得拿着版本号去问服务商到底什么时候上包。

Go 排第二,动作是重编译。 查的不是 go.mod,是 go version,以及你的 CI 用哪条 toolchain 编译。要动就是重新编译、重新发版,改依赖没用。

Node 按条件走。 grep 完 brotliDecompress、确认有没有用 SEA 打包,踩到了就跟 PHP 一起升,没有就按常规节奏。

补丁装完以后

Go 那个看着最轻,反而最容易做假动作,版本号填了、CI 绿了、发版记录也写了,跑的还是带旧 denylist 的二进制。要确认的是「这个二进制是哪条 toolchain 编出来的」,不是「我升到 1.26.10 了没」。

Node 那个急不急,取决于一个很具体的问题。你有没有把别人的数据丢进 Brotli 解压器?有这个动作的,跟 PHP 一起处理;没有的,按常规节奏跟。

还有一件比这三个都靠后的事。10 月 27 日 Chrome 会把 emSign ECC Root CA - G3 整个根摘掉。那天之后如果有人来找你,说某个第三方接口突然报 TLS 错误,先想想是不是这条链,别第一反应去关证书校验。

本博客所有内容无特殊标注均为大卷学长原创内容,复制请保留原文出处。
Built with Hugo
Theme Stack designed by Jimmy