共计 7308 个字符,预计需要花费 19 分钟才能阅读完成。
2026 年 10 月 21 日,[[PHP]] 官方发布了 8.5.12、8.4.27、8.3.36 和 8.2.35 四个版本,一次性修复了两个严重漏洞:CVE-2026-109618 和 CVE-2026-109619。两个漏洞的 CVSS v4.0 评分都是 9.2,在特定条件下,未登录的攻击者可以直接在服务器上执行代码。其中在 Apache + mod_php 的默认配置下,攻击请求甚至在应用代码运行之前就能触发。
[[PHP 基金会]]随后发了一篇长文 Today’s PHP security release, and how we’re strengthening PHP security,交代了漏洞的来龙去脉、为什么修复拖了这么久,以及之后打算怎么改进。这篇文章结合基金会的博客、php.net 的公告和两份 GitHub 安全通告,整理一下这两个漏洞是怎么回事,以及运维上应该怎么处理。
手上有对外提供服务的 PHP,马上升级。官方原话是 "There are no complete mitigations, updating is the only safe choice.",后面提到的配置调整只能降低部分攻击路径的风险,不能代替升级。
受影响版本
| 分支 | 受影响版本 | 修复版本 |
|---|---|---|
| 8.5 | < 8.5.12 | 8.5.12 |
| 8.4 | < 8.4.27 | 8.4.27 |
| 8.3 | < 8.3.36 | 8.3.36 |
| 8.2 | < 8.2.35 | 8.2.35 |
| 8.1 及更早 | 受影响 | 官方不再发布版本,提供了源码级补丁 |
8.1 及更早的版本已经不在官方支持范围内,但考虑到漏洞的严重程度,安全团队还是给 PHP 7.4、PHP 8.0 和 PHP 8.1 提供了最小化的源码补丁,给自己编译 PHP 或者没有 LTS 供应商可以依赖的用户使用。另外 PHP 8.2 会在 2026 年 12 月 31 日结束生命周期,之后就不再有安全更新了,还在 8.2 上的项目可以借这个机会规划升级。
影响范围是所有处理 Web 请求的 PHP 部署方式,公告里点名的有 [[PHP-FPM]]、Apache mod_php、php-cgi、[[FrankenPHP]] 和 [[LiteSpeed]]。官方认为 mod_php 的风险最高,因为在这种模式下已经有了不依赖应用代码、未授权即可触发的利用演示。
CVE-2026-109618:OOM 导致的内存损坏
GitHub 通告编号 GHSA-w8hw-w9fx-j79w。
成因
PHP 在请求超出 memory_limit 的时候,会通过 longjmp() 直接跳出当前的执行流程,中止这个请求。问题出在时机上:引擎里有一些地方会先把请求内存挂到可访问的位置,然后才完成初始化。如果恰好在这个窗口期内发生了任何一次内存分配失败(不一定是这块内存本身的分配),longjmp() 一跳,这块数据就停在了半初始化的状态。
随后请求进入关闭(shutdown)阶段,清理代码会把这块无效数据当成有效数据去读取、判断分支,最终造成内存损坏,并且可以被进一步利用成远程代码执行。通告里列出的 CWE 包括 Use After Free、未初始化指针访问、异常条件处理不当等。
攻击者怎么让 PHP 内存耗尽
直觉上 post_max_size 会挡住大请求,但 multipart 解析里有几个地方可以让请求在看起来没超限的情况下把内存吃光:
- 没有可用
Content-Length的 multipart 请求可以绕过最开始的post_max_size检查,然后持续发送解码后的请求体 - multipart 每个 part 里的 header 数量没有上限,这些 header 存到 PHP 内部后比网络上传输时占用的内存大得多,所以一个没超过
post_max_size的请求照样可以撑爆memory_limit - 空的文件字段不计入
max_file_uploads,但 PHP 依然会为它们构建文件元数据数组,嵌套的字段名还会放大这部分开销 - 此外,任何攻击者能控制的、会导致大量内存分配的应用逻辑都可以作为触发点
不同 SAPI 的暴露程度
- Apache mod_php:已确认可利用。mod_php 即使请求启动失败也会执行 shutdown,而请求体的自动解析发生在启动阶段,所以整个攻击在任何应用代码执行之前就完成了。研究人员在默认的输入限制和内存限制、常规的 prefork 多 worker 配置下验证了未授权 RCE。
- PHP-FPM:请求启动阶段失败时子进程会直接退出,所以启动阶段的 OOM 是安全的。但正常启动之后、应用执行过程中的 OOM 依然会留下无效状态。基金会博客提到 RCE 在 FPM 上通过应用代码路径也得到了确认。
- FrankenPHP:源码分析认为行为和 mod_php 类似,可能存在不依赖应用的攻击路径。
- LSAPI:行为和 FPM 类似。
修复方式
补丁不再在 OOM 时使用 longjmp()。超过 memory_limit 时先记一个「软」OOM 错误,等引擎执行到下一个安全点再抛出;如果内存继续增长、超过另一个更高的阈值,就直接 _exit() 退出进程。这样 shutdown 阶段永远不会看到半初始化的数据,同时内存上限依然有效。
CVE-2026-109619:残留的全局状态导致 Use After Free
GitHub 通告编号 GHSA-6wmf-4rv5-p3rp。
成因
PHP 的 worker 进程会处理很多个请求,进程里有一些全局状态(错误信息、session、对象、扩展的状态等)会指向某个请求自己的内存。按设计,这些全局指针应该在请求内存被释放之前清空。但有两种情况会让清理落空:
- 发生致命错误(包括 OOM)时,某个操作被中途打断,没来得及执行对应的重置
- 在后面的 shutdown 阶段又抛出了一个非致命的 warning,把已经清空的错误状态重新写了进去
结果就是请求的堆内存已经释放了,worker 的全局变量却还指着它。下一个请求进来时,有些代码在正常初始化之前就会读这个旧指针,于是形成 Use After Free。通告里说,配合精心构造的堆布局可以做到完整的远程代码执行。
生产者和消费者
通告把利用过程拆成两个角色:
- 生产者:某个操作把属于请求的状态留在了全局位置。比如解析或编译失败、未捕获的错误、
require失败、Maximum execution time exceeded、OOM,或者扩展清理阶段抛出的诊断信息。 - 消费者:后续请求里,在正常初始化之前就读取、释放或者依据这个状态做判断的代码。
两个请求必须落到同一个 worker 上。worker 数量多、或者 worker 会定期回收,只是降低了单次尝试的成功率,并不能消除风险。
已经确认的消费者有两类。
第一类是请求初始化之前触发的 INI 诊断信息。任何 warning、notice 或者 deprecation 都可以,不需要是致命错误,也不需要被显示或记录到日志。例如设置了 auto_detect_line_endings=1、非默认的 filter.default、PHP 8.3 之后废弃的 assert.* 设置、PHP 8.4 之后废弃的 session 设置、PHP 8.5 里的 report_memleaks=0,或者 memory_limit、date.timezone 的值不合法,又或者日志、session、SOAP 缓存路径被 open_basedir 拒绝。在 mod_php 下,.htaccess 里的 php_value、php_flag 等设置都在 php_request_startup() 之前生效;在 FPM 下,只有前端通过 FastCGI 传 PHP_VALUE / PHP_ADMIN_VALUE 并且触发了诊断信息时才会暴露,pool 配置和 .user.ini 不会成为早期消费者。需要注意的是,调整 error_reporting、用 @ 抑制、关闭 display_errors 或 log_errors 都没有防护作用。
第二类是 session 上传进度(session.upload_progress)。这个功能默认开启,而且不需要应用调用 session_start() 就能进入 session 处理逻辑。上传请求会初始化 session 和解码器的状态,如果这时发生 OOM 或致命错误,重置就被跳过了。官方演示的不依赖应用的触发方式就是在输入处理时制造 OOM,在 mod_php 上整个过程可以在 multipart 启动阶段完成。
另外 PHP 8.5 新增的 fatal_error_backtraces 选项开启时,属于请求的回溯状态在 shutdown 时不会被重置,下一个请求的诊断信息就能碰到它。已经确认的 mod_php RCE 走的就是这条路径。
两个漏洞的评分向量相同:CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N,也就是网络可达、无需权限、无需用户交互,但利用复杂度较高、需要满足一定前提条件。网上一度流传的 10.0 分是错误的。
防御措施
升级
最优先也是唯一完整的方案。
# 确认当前版本
php -v
php-fpm -v
# Debian / Ubuntu 使用发行版或 ondrej/php PPA 的话
sudo apt update && sudo apt install --only-upgrade 'php8.*'
# 升级后重启所有 worker
sudo systemctl restart php8.4-fpm
sudo apachectl graceful
各大 Linux 发行版和 LTS 供应商都提前拿到了补丁,直接检查发行版的更新即可。使用 [[Docker]] 镜像的话,重新 pull 对应 tag 的镜像并重建容器,然后进容器里用 php -v 确认版本号。
有一点值得单独提一下:官方特别点名,如果依赖的是由志愿者维护的 rpms.remirepo.net,这个仓库在发布当天不会有更新,建议切换到发行版自带的软件包或者自己从源码编译,以便第一时间打补丁。
无法立即升级时的缓解措施
以下配置来自官方公告和 GitHub 通告,只能挡住已知的部分攻击路径,不能让服务器变得安全,所以只能作为过渡手段。
针对 CVE-2026-109618:
- 设置
memory_limit=-1,并且要用应用无法覆盖的方式设置,比如 FPM pool 里的php_admin_value或者 Apache 配置,同时检查没有其他 pool、目录或者应用把它改回一个有限值。这样消除了攻击者可控的内存上限失败,但底层的mmap()分配失败依然可能走到有漏洞的代码路径 - 调小
post_max_size。不需要上传文件的应用设为8M,需要上传的设为32M,upload_max_filesize要小于post_max_size - 在 Web 服务器或反向代理层限制解码后的请求大小、输入字段数量、multipart part 数量、header 数量和嵌套深度,并且这些限制要覆盖没有
Content-Length的请求。应用不需要 multipart 表单的话,可以直接在前端拦掉 - 调低
max_input_nesting_level - 用容器或 cgroup 的内存上限(
memory.max)作为兜底,最好配置成只杀掉出问题的 worker,否则整个容器被 OOM Kill 会连 master 一起带走 - 不要用
RLIMIT_AS或RLIMIT_DATA来替代,它们会让mmap()在达到上限时失败,反而触发漏洞
memory_limit=-1 意味着单个请求可以无限吃内存,所以它和 cgroup 限制最好一起使用,否则就是用一个 RCE 换来一个拒绝服务。
针对 CVE-2026-109619:
- 设置
session.upload_progress.enabled=0并重启所有 worker。要在php.ini、FPM pool 或者服务器配置这种请求解析之前就生效的地方设置,不能依赖ini_set()或.user.ini。官方建议即便升级之后,用不到这个功能也保持关闭 - PHP 8.5 设置
fatal_error_backtraces=0并重启所有 worker - 清理那些按请求生效、会在初始化之前输出诊断信息的无效或已废弃 INI 设置,重点检查
.htaccess和前端传入的PHP_VALUE - 修掉攻击者能触发的致命错误和执行超时,包括回调、上传或导入处理、清理逻辑中的失败
- 不在意性能的话,可以考虑 FPM 的
pm.max_requests=1,让每个 worker 只处理一个请求,切断跨请求的状态残留
下面是一个 PHP-FPM pool 配置的示例(以 Debian 系的 /etc/php/8.4/fpm/pool.d/www.conf 为例,按需调整):
php_admin_value[memory_limit] = -1
php_admin_value[post_max_size] = 8M
php_admin_value[upload_max_filesize] = 6M
php_admin_flag[session.upload_progress.enabled] = off
; 仅 PHP 8.5
php_admin_flag[fatal_error_backtraces] = off
Apache mod_php 对应的写法:
php_admin_value memory_limit -1
php_admin_value post_max_size 8M
php_admin_value upload_max_filesize 6M
php_admin_flag session.upload_progress.enabled off
# 仅 PHP 8.5
php_admin_flag fatal_error_backtraces off
改完之后重启所有 worker,然后用 grep -rn memory_limit 之类的方式检查 .user.ini、.htaccess 和代码里的 ini_set() 有没有把 memory_limit 又设回有限值。
排查日志
通告还建议在隔离环境里打开 log_errors=On、error_reporting=E_ALL,并配置可写的 error_log,观察有没有在脚本执行之前出现的 warning 或 deprecation、清理阶段的诊断信息、致命错误和超时。这些都是 CVE-2026-109619 的潜在生产者或消费者。不过日志干净不代表安全,只是帮助发现明显的问题。
为什么修了这么久
这次事件里比较让人意外的是时间线。早在 2024 年,[[Sovereign Tech Agency]] 资助、[[OSTIF]] 组织、[[Quarkslab]] 执行的 PHP 核心安全审计中,一位基金会资助的核心开发者在修复审计问题时就注意到了 OOM 这个根因的迹象。只是当时还只是理论上的风险,没有人知道能利用成 RCE。
安全团队手上有几种修复方案,但每一种都有代价:要么破坏向后兼容,要么引入新的拒绝服务风险,要么需要大量第三方扩展跟着改。团队采用共识决策,熟悉引擎的专家又少,再加上不知道存在 RCE 路径,这件事就这样停停走走拖了好几个月。基金会在文章里直接承认这是「结构性的失败」,之后会改为多数票决策,并鼓励在方案上做折中。
直到 2026 年 8 月 11 日,Calif 的 Ryan A 报告了一个无条件的 RCE,同一天 OpenAI 也独立报告了同一个根因。之后 [[Wiz]](8 月 24 日)和 OpenAI(9 月 18 日)又陆续报告了新的攻击路径,事情才变得紧迫起来。
一次不太顺利的披露
这次的协调披露也出了不少状况:
- 10 月 20 日约 15:00 UTC,两个 CVE 编号连同错误的发布时间和错误的 10.0 评分被泄露到网上,不过没有泄露技术细节。官方在 19:30 UTC 发出预告,并把发布时间从 10 月 22 日提前到 10 月 21 日 14:00 UTC。没法立即发布是因为各发行版还在打包
- 约 20:00 UTC,Calif 误发了 PoC 演示视频,视频被转载传播,加快了外界复现漏洞的进度。23:00 UTC 左右分发给关键基础设施的缓解建议可能也泄露了。不过到正式发布时,安全团队没有发现可用的公开利用代码
- 10 月 21 日 07:41 UTC,[[Chainguard]] 比约定时间提前 6 个多小时发布了带补丁的镜像,打破了禁运期,等于给了攻击者提前分析补丁的机会
PHP 基金会在安全上的投入
基金会这一年在安全上做了不少事情:
- 2026 年 5 月成立了生态安全团队(Ecosystem Security Team),负责分类、协调和加固
- 新增人手:Volker Dusch 担任 AI 安全驻场工程师,Alexandre Daubois 在 7 月加入负责处理安全报告,Daniel Scherzer 在 9 月加入做加固工作
- 工具上,和 [[Alpha-Omega]] 合作用 [[Anthropic]] 的 Claude Mythos 扫描 PHP 源码,用 OpenAI 的 Daybreak Red 验证报告和利用,并为每个支持的平台准备了和 CI 一致的 sanitizer 构建与测试环境
- 审计上,PHP 加入了 OpenAI 的 Patch the Planet 计划,由此促成了 7 月 30 日开始的 Calif 审计,同时 [[Trail of Bits]] 也在审计 PHP
- 从 7 月 1 日起,团队已经发布了 15 个 CVE,处理了 100 多份无效、重复或无法复现的报告。AI 帮忙做分类,但每个决策和每个补丁都由人来完成
- 以后卡住的疑难修复会更早上报给基金会管理层
最后
这两个漏洞都出在内存管理和错误处理这类几乎每个请求都会经过的基础路径上,这也是它们危险、同时又难修的原因。对使用者来说要做的事情很简单:
- 立即升级到 8.5.12、8.4.27、8.3.36 或 8.2.35
- 还在用 8.1 及以下版本的,尽快找发行版或 LTS 供应商的补丁,或者直接升级到受支持的版本
- 用 mod_php 的优先处理,它的风险最高
- 用不到的功能(比如 session 上传进度)顺手关掉,multipart 请求在前端做好限制
参考
- Today’s PHP security release, and how we’re strengthening PHP security
- php.net 2026 年新闻归档
- GHSA-w8hw-w9fx-j79w: RCE via OOM corruption
- GHSA-6wmf-4rv5-p3rp: RCE via stale global state
- PHP Core Security Audit Results
