2026 年 10 月 21 日,PHP 官方发布紧急安全更新,修复了两个 CVSS v4.0 评分均为 9.2 的未认证远程代码执行漏洞,社区将其合称为“PHP2Shell”。这两个漏洞的公开技术细节在协调发布前已被提前泄露,且已有可用的利用研究流出,留给运维和开发团队的窗口非常有限。
漏洞速览:两个 9.2 分的未认证 RCE
两个漏洞的成因不同,但后果相同:远程未认证攻击者可以在受影响系统上执行任意代码。
| 漏洞 | 核心成因 | 触发路径 |
|---|---|---|
| CVE-2026-109618 | 内存不足(OOM)处理时 longjmp 跳转导致 request 内部状态失效,后续清理阶段将不完整数据当作正常数据继续处理 | 可通过消耗 memory_limit 触发 |
| CVE-2026-109619 | 请求使用的内存在释放后,指向该内存的内部状态未被清理,同一 PHP-FPM worker 处理的下一个请求可能读到残留的“陈旧全局状态” | 与 session.upload_progress 机制相关,该特性自 PHP 5.4 起默认开启 |
CVE-2026-109619 的入口是 session.upload_progress,这使得 PHP-FPM / mod_php 等 Web SAPI 的风险显著高于纯 CLI 场景。
影响版本:所有版本
根据 SK-CERT 和 BSI 的通报,漏洞影响 PHP 5.0 至 8.1 的所有版本,8.2 及以上分支的早期版本同样在影响范围内。
有官方修复版本的稳定分支
| PHP 分支 | 修复版本 |
|---|---|
| 8.5 | 8.5.12 |
| 8.4 | 8.4.27 |
| 8.3 | 8.3.36 |
| 8.2 | 8.2.35 |
EOL 分支:官方只提供“最小源码回补”,没有发布版本
PHP 7.4、8.0、8.1 早已结束生命周期,官方不会再为它们发布常规的安全更新。但鉴于这两个漏洞的严重性,PHP 安全团队破例为这三个分支提供了 minimal source backports(最小化源码回补),以 Git 分支形式存在,面向的是从源码编译 PHP 的用户。
这些分支的命名规则是:
php-src 分支:PHP-7.4-CVE-2026-109618-109619
PHP-8.0-CVE-2026-109618-109619
PHP-8.1-CVE-2026-109618-109619它们不是发布包,不是 tarball,也没有对应的 Docker 官方镜像。 如果你使用的是 php:7.4-fpm 这类 EOL Docker 镜像,直接 docker pull 不会得到任何修复。
补救措施:按“能否升级”分层处理
第一优先级:升级到受支持的 PHP 版本(如有条件)
如果应用代码兼容,升级到 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 或更高是最干净的路径。官方 Docker 镜像只覆盖 8.2 及以上,升级后可以直接获得持续的安全更新。
第二优先级:在无法立即升级时,使用已回补的社区镜像
对于必须停留在 7.4 / 8.0 / 8.1 的项目,社区已经提供了开箱即用的已回补镜像,可以直接替换 FROM 指令,无需自己编译。
mpyw/php-eol-CVE-2026-109618-109619 是目前覆盖最完整的方案:
FROM ghcr.io/mpyw/php-eol-cve-2026-109618-109619:7.4-fpm-alpine可用标签(与官方命名习惯一致):
7.4-fpm-alpine
7.4-fpm-bullseye
7.4-cli
8.0-fpm-alpine
8.1-fpm-bullseyempyw 镜像的构建方式与官方上游对齐:下载 php.net 官方 tarball,验证 SHA-256 和 GPG 签名,从 patches/ 应用修复,再重新打包 tarball 使 docker-php-source extract 也能得到已打补丁的源码。维护者明确标注:仅回补了这一个修复,这些 PHP 版本的其他已知问题依然存在,应规划升级。
waksboy/fixed-fpm-cve-2026-109618-and-109619-images 是另一个选择,覆盖 PHP 5.0 到 8.1,7.4 的修复源码同样来自官方 PHP-7.4-CVE-2026-109618-109619 分支,并提供 :7.4-fpm-bullseye 等标签。
第三优先级:临时缓解(不能替代补丁)
在补丁尚未进入运行时之前,可以在 PHP-FPM 池配置中关闭 session.upload_progress。多个安全厂商的早期指引都指向同一设置:
; FPM pool 配置(www.conf)
php_admin_flag[session.upload_progress.enabled] = off同时建议配合:
php_admin_value[post_max_size] = 20M
php_admin_value[memory_limit] = 200M并在 Web 服务器层限制请求体大小。这只阻断 CVE-2026-109619 的一条已知路径,不能消除 CVE-2026-109618,不能替代源码补丁。
为 PHP 7.4 生成 Patch
官方没有提供 7.4-CVE-2026-109618-109619.patch 文件,但你可以从官方回补分支直接生成。
方式一:GitHub Compare 生成 diff(推荐)
官方分支的基线是 php-7.4.33。用 GitHub 的 compare 接口生成两个 ref 之间的差异:
curl -L -o 7.4-CVE-2026-109618-109619.patch \
"https://github.com/php/php-src/compare/php-7.4.33...PHP-7.4-CVE-2026-109618-109619.diff"根据社区核对,这个 diff 涉及 19 个文件、80 个 hunk。生成后务必确认文件非空,且开头是 diff --git 格式,以保证 patch -p1 可以正确应用。
方式二:直接使用社区已整理好的 patch
waksboy 仓库提供了 patches/cve-74.patch,注明适用于 7.4.33 和 7.4.29,来源为官方分支。mpyw 仓库的 patches/7.4-CVE-2026-109618-109619.patch 同样来自官方分支,对应 commit 712e60f9ed。
两个来源都可以,但方式一的 diff 与 7.4.33 的匹配度最确定,适合自编译时使用。
编译 PHP 7.4.33 FPM 镜像(修复版)
最稳妥的做法是以官方 php:7.4.33-fpm-alpine 为基础,只重新编译 PHP 核心并打入补丁,而不是从 alpine 裸系统从头构建。官方镜像里已经包含 docker-php-ext-* 辅助脚本和正确的运行时依赖,直接复用可以避免大量兼容性问题。
最小可用 Dockerfile 骨架
FROM php:7.4.33-fpm-alpine
# 编译依赖
RUN apk add --no-cache --virtual .build-deps \
$PHPIZE_DEPS \
curl-dev libedit-dev libressl-dev libsodium-dev \
libxml2-dev sqlite-dev linux-headers \
zlib-dev libpng-dev freetype-dev libjpeg-turbo-dev \
libwebp-dev libxpm-dev oniguruma-dev
# 下载 7.4.33 源码
ENV PHP_VERSION=7.4.33
RUN set -eux; \
curl -fsSL -o /tmp/php.tar.xz \
"https://museum.php.net/php7/php-${PHP_VERSION}.tar.xz"; \
mkdir -p /usr/src/php; \
tar -xf /tmp/php.tar.xz -C /usr/src/php --strip-components=1; \
rm /tmp/php.tar.xz
# 应用 CVE-2026-109618 / 109619 补丁
COPY 7.4-CVE-2026-109618-109619.patch /tmp/
RUN set -eux; \
cd /usr/src/php; \
patch -p1 < /tmp/7.4-CVE-2026-109618-109619.patch; \
rm /tmp/7.4-CVE-2026-109618-109619.patch
# 用与官方 7.4.33 一致的 configure 参数重新编译
WORKDIR /usr/src/php
RUN set -eux; \
./configure \
--build="$gnuArch" \
--with-config-file-path="$PHP_INI_DIR" \
--with-config-file-scan-dir="$PHP_INI_DIR/conf.d" \
--enable-option-checking=fatal \
--with-mhash --with-pic --enable-ftp --enable-mbstring \
--enable-mysqlnd --with-password-argon2 --with-sodium=shared \
--with-pdo-sqlite=/usr --with-sqlite3=/usr \
--with-curl --with-libedit --with-openssl --with-zlib \
--with-fpm-user=www-data --with-fpm-group=www-data \
; \
make -j"$(nproc)"; \
make install; \
make clean; \
cp -v php.ini-* "$PHP_INI_DIR/"; \
rm -rf /usr/src/php
RUN apk del --no-network .build-deps社区方案汇总
| 方案 | 类型 | 适用场景 | 关键信息 |
|---|---|---|---|
| mpyw/php-eol-CVE-2026-109618-109619 | 预构建镜像 | 希望最快替换 FROM,不自己编译 | 覆盖 7.4/8.0/8.1,Debian + Alpine 多变体,镜像基于官方回补分支构建 |
| waksboy/fixed-fpm-cve-2026-109618-and-109619-images | 预构建镜像 + patch 文件 | 需要 patch 文件,或覆盖 5.0–8.1 更广范围 | 7.4 patch 为 patches/cve-74.patch,官方分支来源 |
| pruiz/php-CVE-2026-109618 | 自编译参考 | 需要完全掌控构建流程、审计每一步 | Fork 官方 7.4 Dockerfile,在 configure 前应用 *.patch,保留官方 GPG/SHA256 校验 |
| vperetti/php-cve-2026-109618-109619-backports | 自编译 patch | 需要 7.3 及更早的非官方回补 | 提供 php-7.3-CVE-2026-109618-109619.patch,镜像官方 7.4 回补 commit 712e60f9ed |
如果选择自编译而不是使用 mpyw 预构建镜像,pruiz 的结构最值得参考:它保留了官方 7.4.33 Dockerfile 的 configure 参数和辅助脚本,只增加了一个 patch 应用步骤,可重现性和可审计性最好。
验证修复是否真正生效
替换镜像或重新编译后,不要只看构建是否成功。至少做两层验证:
第一层:版本与配置检查
docker run --rm my-php:7.4-patched php -v
docker run --rm my-php:7.4-patched php-fpm -t第二层:用 Docker Scout 或 Trivy 扫描镜像
docker scout cves --only-cve-id CVE-2026-109618,CVE-2026-109619 my-php:7.4-patched如果扫描器显示这两个 CVE 仍在,说明补丁没有真正进入最终运行阶段的二进制文件——多阶段构建时尤其容易发生:构建阶段打了补丁,但最终阶段复制的仍是旧镜像里的 /usr/local/bin/php。
第三层:行为验证(可选但推荐)
waksboy 的测试思路值得借鉴:在触发 memory_limit 耗尽后,观察请求结束阶段的清理行为是否与未修复镜像产生差异。这比版本字符串更能证明 CVE-2026-109618 的修复实际生效。
最后提醒一点:预构建社区镜像和自编译补丁都只是“止血”。7.4 的其他已知问题不会因为这一个补丁而消失,把“升级到 8.2+”作为明确的技术债跟踪,仍然是这类 EOL 项目不能跳过的下一步。
