PHP 未授权 RCE 应急响应:CVE-2026-109618 / CVE-2026-109619 排查与加固实战

安全

2026-10-21,PHP 官方凌晨发布带外安全版本,修复两个 CVSS v4.0 评分 9.2 的未认证远程代码执行漏洞,且技术细节在发布前数小时已泄露、社区已出现利用迹象。本文是一份从"自查 → 临时缓解 → 验证 → 升级排期"的完整 SOP,适合直接照做。

php2shell

一、漏洞背景:发生了什么

2026-10-21,PHP 官方发布 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35,修复两个漏洞:

CVE 类型 原理(一句话)
CVE-2026-109618 OOM 内存损坏 RCE 内存耗尽使 PHP 中断时请求数据只初始化了一半,请求清理阶段把"无效数据"当有效数据处理
CVE-2026-109619 残留全局状态 RCE 上一个请求的内部状态在内存释放后仍残留,同一 worker 的后续请求用到脏状态(已确认路径指向 session.upload_progress 特性)

三个关键事实,先对齐认知:

  • 影响面:所有处理 Web 请求的 PHP(PHP-FPM、mod_php、php-cgi、FrankenPHP、LiteSpeed)都受影响,其中 mod_php 风险最高,已有"未认证请求在应用代码执行前即可触发"的利用演示;
  • "我的站没有文件上传功能"≠ 不受影响:触发面在 PHP 引擎内部,应用代码根本没参与,session.upload_progress 是 PHP 5.4 起自带且默认开启的特性,POST 一个同名字段即可触发;
  • 官方原话:"There are no complete mitigations, updating is the only safe choice."——下文所有配置修改都只是临时缓解,不是修复。

二、第一步:自查环境

1. 确认 PHP 版本

php -v

升级目标:8.5.12 / 8.4.27 / 8.3.36 / 8.2.35;8.1 及以下已停止维护,需找发行版/LTS 回移植补丁或官方 7.4 / 8.0 / 8.1 源码回移植分支。

2. 确认到底加载的是哪个 php.ini

php --ini

看 Loaded Configuration File 和 Scan for additional .ini files in 两行。两个经典坑:

  • CLI 和 FPM 用不同 php.ini:Debian/Ubuntu 上是 /etc/php8.x/cli/php.ini 与 /etc/php8.x/fpm/php.ini 两套独立文件,改错文件白改;
  • 官方 Docker PHP 镜像默认没有 php.ini:输出是 Loaded Configuration File: (none),只加载 conf.d/ 目录。此时所有参数走编译内置默认值,需要自建配置(见第三节)。

三、第二步:临时缓解(升级窗口期内执行)

配置必须写在 php.ini / FPM pool / 服务器配置(请求解析前生效)层。官方明确:不要依赖 ini_set() 或 .user.ini,对本漏洞不可靠。

1. 针对 CVE-2026-109619

session.upload_progress.enabled = Off
session.use_strict_mode = 1
  • 前一条是官方点名的缓解:堵住已确认的 session 攻击路径。注意官方措辞是"blocks the confirmed session paths but not other variants";
  • 后一条是纵深防御:严格模式拒绝客户端传入的、服务端从未创建过的会话 ID,防会话固定/会话伪造注入。它不针对本次 CVE,但是值得长期保留的加固项。

2. 针对 CVE-2026-109618

memory_limit = -1
post_max_size = 8M        ; 有文件上传需求则设 32M
upload_max_filesize = 8M  ; 必须保持在 post_max_size 之下
  • memory_limit=-1 去掉"可控的"内存限制失败,但官方提示底层分配失败仍可能进入漏洞路径,所以是缓解不是免疫;
  • post_max_size 是请求体上限,在解析层直接砍掉大请求的攻击空间;
  • memory_limit 是 PERDIR 项,应用代码 ini_set() 和 .user.ini 都能覆盖它。要做到"应用改不了",必须放进 FPM pool:
; /etc/php8.x/fpm/pool.d/www.conf(各发行版路径略有差异)
php_admin_value[memory_limit] = -1

3. Web 服务器 / 代理层限流

client_max_body_size 8m;

同时在网关层限制请求体大小、表单字段数、multipart 分片数、header 数量与嵌套深度——官方特别强调这些限制要覆盖没有 Content-Length 的请求。

4. Docker 环境完整示例

官方镜像默认无 php.ini,往 conf.d 加文件最干净:

cat > /usr/local/etc/php/conf.d/99-session-hardening.ini <<'EOF'
session.upload_progress.enabled = Off
session.use_strict_mode = 1
memory_limit = -1
post_max_size = 8M
upload_max_filesize = 8M
EOF

容器重建会丢配置,务必固化进 Dockerfile:

COPY session-hardening.ini /usr/local/etc/php/conf.d/99-session-hardening.ini

5. 硬性要求:重启全部 worker

不要用 reload。CVE-2026-109619 的脏状态在 worker 内存里,reload(HUP)保留旧 worker,清不干净:

systemctl restart php8.3-fpm     # 或:docker restart <容器>

四、第三步:验证

php -r 'var_dump(ini_get("session.upload_progress.enabled"), ini_get("session.use_strict_mode"), ini_get("memory_limit"), ini_get("post_max_size"));'

期望输出:

string(1) "0"
string(1) "1"
string(2) "-1"
string(2) "8M"

两个实战小坑:

  • 若 upload_progress.enabled 返回空串而不是 "0",说明加载的某个配置文件里有一行值写空的指令(如 session.upload_progress.enabled =)。用 grep -rn "upload_progress" /usr/local/etc/php/ 定位后整体重写,必要时 cat -A 检查 CRLF/BOM 等不可见字符;

  • CLI 验证通过后,还要在 Web 端放一个 info.php(<?php phpinfo();)确认 FPM 实际加载的值,确保改的是对外服务的那个 SAPI。

20261022090350

五、业务影响评估(回归清单)

配置 潜在业务影响
upload_progress.enabled=Off 若有前后端依赖 $_SESSION['upload_progress_*'] 展示上传进度,会失效;现代前端普遍用 XHR upload.onprogress(浏览器端进度),不受影响
use_strict_mode=1 "外部传入 PHPSESSID 恢复会话"的场景(深链、App 跳 H5)会掉登录态;多节点 + 本地文件会话未集中存储时,用户会随机掉登录
post_max_size=8M 超过 8M 的上传请求被整体拒绝,有大文件业务的按上面说明提到 32M
memory_limit=-1 取消脚本内存上限,依赖容器 cgroup 限制兜底即可

上线前建议:全局搜一遍代码里的 upload_progress 与 ini_set('memory_limit';有条件的话把会话迁移到 Redis/DB 再开严格模式;变更后盯 1–2 天登录成功率与上传错误率。

六、终局:升级版本

当前版本 动作
8.2 – 8.5 升级到 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12(Docker 直接换基础镜像 tag 重打)
8.1 及以下 已 EOL:找发行版/LTS 回移植,或用官方 7.4 / 8.0 / 8.1 源码回移植分支,同时规划迁移到受支持版本
特别注意 8.2 将于 2026-12-31 EOL,之后不再有任何安全修复,建议直接规划跳 8.3+

升级完成后,use_strict_mode=1 与 upload_progress=Off 建议保留——它们是好的长期加固,不是临时补丁。

七、一页 Checklist

  • [ ] 排查 PHP 版本与 SAPI(FPM / mod_php / CGI),逐个节点
  • [ ] 确认对外 SAPI 加载的 php.ini(含 Docker "none" 状态)
  • [ ] 改配置,memory_limit=-1 用 pool php_admin_value 锁死防覆盖
  • [ ] Nginx / 网关层请求体与字段数限流
  • [ ] restart 全部 worker(不是 reload)
  • [ ] CLI + Web 双重验证,期望值 0 / 1 / -1 / 8M
  • [ ] 业务回归:文件上传、登录态保持、搜 upload_progress 依赖
  • [ ] 配置固化进 Dockerfile / 配置管理
  • [ ] 升级版本排期(建议本周内)

八、雷池WAF规则

动作选“黑名单”,勾选“启用观察模式:发现攻击后仅记录攻击事件,不拦截”。

  • 条件组 1(组内 AND):
    • POST 参数:PHP_SESSION_UPLOAD_PROGRESS,操作符“存在”
    • POST 参数:PHPSESSID,操作符“正则”,值 ^[0-9A-Za-z,-]{16,256}$
    • 请求头:Content-Type,操作符“包含”,值 multipart/form-data
      点“添加或条件”
  • 条件组 2(组内 AND):
    • POST 参数:PHP_SESSION_UPLOAD_PROGRESS,操作符“存在”
    • 请求头:Content-Type,操作符“包含”,值 multipart/form-data
    • 请求 Body,操作符“正则”,值:Content-Disposition:\s*form-data;\s*name="[^"]+"\r\n([^\r\n]+\r\n)*\r\n[\s\S]{0,32}\x00

20261022103304

JSON

{
  "name": "PHP Core RCE CVE-2026-109618/109619 - observe",
  "action": 1,
  "monitor": true,
  "log": false,
  "is_enabled": true,
  "insert_position": "first",
  "pattern": [
    [
      {"k": "post_param", "op": "exist", "v": [], "sub_k": "PHP_SESSION_UPLOAD_PROGRESS"},
      {"k": "post_param", "op": "re", "v": ["^[0-9A-Za-z,-]{16,256}$"], "sub_k": "PHPSESSID"},
      {"k": "req_header", "op": "has", "v": ["multipart/form-data"], "sub_k": "Content-Type"}
    ],
    [
      {"k": "post_param", "op": "exist", "v": [], "sub_k": "PHP_SESSION_UPLOAD_PROGRESS"},
      {"k": "req_header", "op": "has", "v": ["multipart/form-data"], "sub_k": "Content-Type"},
      {"k": "req_body", "op": "re", "v": ["Content-Disposition:\\s*form-data;\\s*name=\"[^\"]+\"\\r\\n([^\\r\\n]+\\r\\n)*\\r\\n[\\s\\S]{0,32}\\x00"], "sub_k": ""}
    ]
  ]
}

参考资料