2026 年 10 月 21 日 13:30 UTC(北京时间 21:30 左右),PHP 官方发布 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35,修复两个未认证远程代码执行漏洞:CVE-2026-109618(GHSA-w8hw-w9fx-j79w)与 CVE-2026-109619(GHSA-6wmf-4rv5-p3rp),社区代号 PHP2SHELL,CVSS v4.0 均为 9.2。

官方公告里有一句话值得先记住:「There are no complete mitigations, updating is the only safe choice.」——没有完整的缓解方案,升级是唯一安全选择。所以下面所有配置类手段,定位都是「降低暴露面」,不是「修好了」。

时间线:从细节泄露到补丁,不到 24 小时

时间事件
10 月 20 日PHP 项目预告将发布安全版本;漏洞细节在补丁前已在网上流传,部分报道称官方被迫提前发布
10 月 21 日 13:30 UTC8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 发布,同时公开两个 GHSA 技术公告
10 月 21 日当天下半夜补丁发布数小时内出现 PoC 与大规模扫描;CERT-EU、ENISA 等机构陆续发布公告与联合声明
10 月 22 日国内 CNVD 收录为 CNVD-2026-41674、CNVD-2026-41675,多家云厂商与高校信息化部门发布风险通告

这个节奏意味着:不要用「等官方出利用分析再动手」的惯性处理这次事件。补丁与 PoC 几乎同日出现,暴露窗口以小时计。

两个漏洞,分别坏在哪一环

CVE-2026-109618(GHSA-w8hw-w9fx-j79w):OOM 中断留下的「半初始化状态」

PHP 在内存分配失败(OOM)时会用 longjmp() 提前跳出,放弃继续处理当前请求。问题在于:中断发生时,内部请求数据可能只初始化了一部分,而这段内存已经可以被引用。随后请求清理阶段会把这批无效数据当成正常数据处理,于是内存被破坏。

触发方式很「便宜」:攻击者构造一个 multipart 请求,用大量头部、深层嵌套的字段名或空文件控件,在看起来没超过 post_max_size 的情况下把 memory_limit 耗光。

CVE-2026-109619(GHSA-6wmf-4rv5-p3rp):跨请求残留的「过期状态」

PHP-FPM、mod_php 这类常驻进程会复用 worker。请求私有的内部状态(错误、会话、对象、扩展状态)在底层内存释放后仍然存活;同一个 worker 处理的下一个请求,可能在正常初始化重置之前就读到了这个过期状态,形成释放后使用(use-after-free)。

利用方式是双请求协作:第一个请求当「生产者」,把堆布局摆成特定形状;第二个请求当「消费者」,去访问已经失效的内存。整个过程不需要登录,也不需要诱导任何人点击链接。

谁能真的被打穿:mod_php 已被实证,FPM 还没有完整链

这一点常被二手文章写混,直接影响处置优先级:

  • Apache + mod_php:已在 HTTP 场景下实证未认证 RCE,而且发生在应用代码执行之前——也就是说,框架、路由、中间件都还没有机会运行;
  • PHP-FPM:源码分析表明同样存在易感性,但公开资料里还没有完整可用的利用链;
  • php-cgi、FrankenPHP、LiteSpeed 同属受影响范围,处置上按 FPM 一类对待。

所以「FPM 还没被打穿」不等于安全:这只是当前公开研究的进度,不是架构上的免疫。

为什么业务层防不住

  • 触发点在解释器内部的错误处理路径,WAF 看到的只是一个格式合法的 multipart 上传;
  • 不经过任何业务代码判断,if (!isAdmin()) 之类的防护完全无效;
  • 内存破坏发生在请求生命周期里,与应用是 Laravel、ThinkPHP 还是 WordPress 无关。

先判断你在哪一档

分支受影响版本修复版本
PHP 8.5< 8.5.128.5.12
PHP 8.4< 8.4.278.4.27
PHP 8.3< 8.3.368.3.36
PHP 8.2< 8.2.358.2.35(该分支 2026-12-31 EOL)
8.1 及更早全部,官方不再维护仅有发行版回溯包,或官方为 7.4 / 8.0 / 8.1 提供的最小源码补丁

按风险从高到低排,资源有限时按这个顺序推:

  1. 最高:Apache + mod_php 且对外暴露 80/443。能切 PHP-FPM 就先切,切之前优先升级;
  2. 高:PHP-FPM 且存在匿名上传、评论、投稿、导入等入口的站点——攻击者本来就有合法途径发 multipart;
  3. 中:FPM 部署、严格限制上传接口、前置有 CDN 或 WAF;
  4. 低但仍需处理:纯内网或只跑 CLI 的 PHP。CLI 不经过 HTTP 请求生命周期,但不能以此无限期拖延。

自查时注意多版本共存的面板环境,别只看 php -v 的输出了事:

php -v

# 面板通常装了多个版本,确认 FPM / Apache 实际加载的是哪一个二进制
ls -l /www/server/php/*/bin/php
ps aux | grep -E 'php-fpm|httpd|apache2' | grep -v grep

# 发行版包:核对包管理器里的实际版本
dpkg -l | grep php | head -20
rpm -qa | grep -E '^php' | head -20

别忘了两类容易漏的资产:容器镜像里的 PHP,以及后台依赖 PHP 的商业软件 / 开源产品。ENISA 的公告里点名了 NCSC-NL 维护的 php2shell 清单,专门用来比对哪些第三方产品内嵌了受影响版本的 PHP。

升级:唯一完整修复,但有四个坑

  1. 选对包源:优先用发行版或面板的安全更新;rpms.remirepo.net 这类志愿者仓库在发布当天不会有更新,官方直接建议改用发行版包或自行编译;
  2. 重启所有 worker:升级后不重启 FPM / Apache,内存里跑的仍是旧代码。队列 worker、常驻的定时进程、swoole / roadrunner 这类常驻进程也要一并重启;
  3. 验证运行时版本:看 phpinfo() 或 FPM 实际加载的二进制,而不是看安装包版本号;
  4. 回归业务:上传、图片处理、数据库连接、缓存、队列消费逐项过一遍,尤其是上传路径——它正是这次漏洞的触发面。

如果还在 PHP 8.2,注意它 2026-12-31 就该 EOL 了。这次升级顺手挪到 8.3 或 8.4,比两个月后再来一轮要划算。

不能立刻升级时的官方缓解

官方原文说得很清楚:以下措施只降低特定攻击路径的暴露面,不能使安装变安全。按漏洞分开看。

针对 CVE-2026-109618(OOM 路径)

  • 取消可控的内存上限:设置 memory_limit=-1,并让它无法被应用覆盖——用 FPM pool 的 php_admin_value 或 Apache 配置下发,然后确认没有任何 pool、目录或应用级设置把有限值重新写回去。注意这只消除了「可控的 PHP 内存上限失败」,底层分配失败仍可能触达漏洞路径。
  • 收紧请求体:不需要上传的站点 post_max_size = 8M,需要上传的 32M,并保持 upload_max_filesize 小于 post_max_size。
  • 在 Web 服务器或代理层限制:解码后的请求体大小、输入字段数量、multipart part 数量、header 数量、嵌套深度。关键是这些限制要覆盖没有 Content-Length 的请求(chunked 传输),否则攻击者可以绕开。
  • 用 cgroup 做可用性边界:策略应尽量只终止受影响的 worker。不要用 RLIMIT_AS / RLIMIT_DATA 替代——它们本身就会制造漏洞所依赖的分配失败,等于帮攻击者触发条件。
; FPM pool 里下发,应用无法用 ini_set 覆盖
php_admin_value[memory_limit] = -1

; php.ini / pool / Apache 配置均可
post_max_size = 8M
upload_max_filesize = 4M

针对 CVE-2026-109619(过期状态路径)

  • 关掉上传进度:session.upload_progress.enabled = 0,改完重启所有 worker。必须在 php.ini、FPM pool 或服务器配置里于请求解析之前生效;不要依赖 ini_set() 或 FPM 的 .user.ini。它只阻断已确认的 session 路径,不覆盖其他变体。
  • PHP 8.5 额外一步:fatal_error_backtraces = 0,同样要重启 worker。
  • 清理初始化前会产生诊断的配置:移除无效或已弃用的按请求设置。用 error_reporting、display_errors 或 @ 把报错藏起来是无效的——诊断信息本身已经在初始化前产生了。
  • 修复可被触达的致命错误与超时:回调、上传 / 导入处理、清理阶段的失败都要收敛。
session.upload_progress.enabled = 0
fatal_error_backtraces = 0   ; 仅 PHP 8.5 需要

兜底

  • 实在升不了的互联网暴露系统,先从公网隔离(下线、或收进访问控制之后),这是 ENISA 联合声明里给的保守做法;
  • WAF 规则能挡已知变种,挡不住绕过 CDN 直连源站的流量,也挡不住改写后的请求;
  • session.use_strict_mode = 1 属于常规加固,顺手打开没有坏处,但别把它当成这次漏洞的解法。

升级之外:怎么判断有没有被打

细节在补丁前就泄露了,所以你看到的异常可能早于你打的补丁。升级不能替代排查:

  1. PHP-FPM / Apache 的 worker 是否出现异常崩溃与频繁重启;
  2. OOM 类致命错误、warning 与 deprecation 是否突然增多,尤其是伴随上传 / multipart 请求时;
  3. PHP 进程树里是否冒出 sh、bash、curl、wget、python 等异常子进程;
  4. Web 目录近期新增 / 修改的 .php 文件、非预期的文件写入;
  5. crontab -l、/etc/cron.d/ 与 systemd 里是否有陌生任务;
  6. 访问日志里密集的 multipart POST:头部异常多、字段名超长、带空文件控件、缺失 Content-Length。

确认被入侵的机器,优先重装系统并重建服务。只删文件几乎一定复发——webshell 通常是多点布置的。

几个常见误区

  • 「框架新就没事」:漏洞在解释器层,Laravel、Symfony、WordPress、Drupal 一样中招。
  • 「升级了就等于修好了」:没重启 worker 等于没升级。
  • 「改了 php.ini 就生效」:session.upload_progress 不支持 ini_set,.user.ini 也不算数。
  • 「把报错关掉就看不见了」:诊断信息在初始化前已生成,隐藏不解决问题。
  • 「用 RLIMIT 限内存更安全」:正好相反,它会制造漏洞需要的分配失败。
  • 「FPM 还没被打穿,可以等等」:这是研究进度,不是架构免疫;公网暴露面优先处理。
  • 「EOL 版本没人打」:长期未升级的存量站点恰恰是自动化扫描的首选目标。

可以直接抄的落地清单

  1. 定优先级和时限:互联网暴露系统 24–48 小时内完成升级,内网 7 天内;确实修不了就先隔离(这个时限是我们的内部 SLA,官方只说了「立即」);
  2. 建 PHP 版本台账:主机、容器镜像、面板多版本、第三方产品一个都不能漏;
  3. 把「升级 + 重启 + 验版本 + 回归」固化成一个原子流程,写成脚本或检查单;
  4. 复核四组配置:memory_limit、post_max_size / upload_max_filesize、session.upload_progress.enabled、fatal_error_backtraces(8.5);
  5. 把 multipart 入口列成清单:匿名上传、评论带图、导入接口,逐个确认大小与字段数限制。

参考(官方优先)

  • PHP 官方公告与下载:php.net/archive/2026.php
  • CVE-2026-109618:GHSA-w8hw-w9fx-j79w
  • CVE-2026-109619:GHSA-6wmf-4rv5-p3rp
  • CERT-EU Security Advisory 2026-018(缓解措施细节最全)
  • ENISA 等机构关于 PHP 严重漏洞的联合声明(2026-10-21)
  • NCSC-NL php2shell 清单:排查内嵌 PHP 的第三方产品
  • CNVD-2026-41674 / CNVD-2026-41675

本文的版本号、触发条件与缓解参数来自 php.net 公告、两个 GHSA 与 CERT-EU 公告整理。实际处置前请再对照一次官方原文与你所用发行版的安全通告——尤其是 EOL 分支,能拿到什么补丁取决于发行版而不是 PHP 项目。