
2026 年 10 月 21 日,PHP 官方做了一件很少见的事:把原定晚一天的补丁提前发了。原因是两个远程代码执行漏洞的技术细节在补丁发布前就泄露到了公开渠道,PoC 已经在流传。这两个漏洞被业界合称 PHP2Shell,编号 CVE-2026-109618 与 CVE-2026-109619,CVSS v4.0 评分都是 9.2。
接下来的一周半,事情发展得比多数人预想的快:CISA 在 10 月 28 日确认漏洞正在被积极利用,要求美国联邦民事机构三天内完成修复;公开网络测绘数据显示,仅按会话特征筛选就有约 49.9 万个暴露资产;国内 CNVD 同期收录为 CNVD-2026-41674 与 CNVD-2026-41675,各大主机面板、建站厂商陆续发公告。
这篇文章只讲一件事:怎么把它真正修好。不是「建议尽快升级」这种场面话,而是从确认版本、选版本、升级、重启到验证生效的完整流程,以及每一步容易漏掉的地方。因为这次漏洞有个特点——很多人以为自己修好了,其实没有。
一、先看清楚这次漏的是什么:两个 CVE,一个共同点
要理解为什么这次必须升级、且必须重启,得先知道它伤在哪。两个漏洞都在 PHP 解释器内核,不在某个框架或 CMS 里。
CVE-2026-109618 是内存管理路径的问题。攻击者在请求启动阶段、也就是表单解析(multipart)阶段,就能构造请求把内存打到耗尽。正常的 OOM 会触发 bailout(异常中断),但这次的问题在于——bailout 跳过了清理逻辑,PHP 带着一份已经损坏的内存状态继续往下走关闭流程。这段代码路径在默认配置下就可达。
CVE-2026-109619 是请求生命周期的问题。PHP 每个请求结束后要重置全局状态,但前一个请求如果发生致命错误(内存耗尽、超时、解析错误都算),重置会被打断。残留的悬空指针就在那儿放着,等下一个请求初始化之前被访问,构成释放后使用(Use-After-Free)。
两者都指向同一个入口:session.upload_progress 机制,也就是文件上传进度条功能。它默认是开启的,而且——这点最关键——不需要你的应用调用 session_start(),也不需要你的代码有任何缺陷。攻击者只要发一个精心构造的 multipart/form-data 请求就行,不用登录、不用认证、不用受害者点任何东西。
实测数据:在 PHP 默认的 memory_limit = 128M 配置下,约 4MB 的恶意请求体就能在请求解析阶段把内存耗尽。同一条请求打到 8.5.12 上,什么都不会发生。
这就是它可怕的真正原因:过去我们说「你的 CMS 有漏洞」,意思是某段业务代码写错了;这次是语言运行时本身的问题。只要你的服务器上跑着受影响版本的 PHP,无论上面挂的是 WordPress、DedeCMS、自研系统,还是就一个 index.php 探针,全都在射程内。
二、影响范围:对照版本号自查
先把范围说清楚。受影响的是 PHP 5.4.0 到 8.1.x 的全部历史版本,以及三个仍在维护分支里低于补丁线的小版本:
| 版本分支 | 受影响范围 | 修复版本 | 备注 |
|---|---|---|---|
| PHP 8.5 | 低于 8.5.12 | 8.5.12 | 最新主线,额外有 fatal_error_backtraces 缓解项 |
| PHP 8.4 | 低于 8.4.27 | 8.4.27 | 当前生产环境推荐位 |
| PHP 8.3 | 低于 8.3.36 | 8.3.36 | 兼容性压力小,老项目优先 |
| PHP 8.2 | 低于 8.2.35 | 8.2.35 | 2026-12-31 停止维护,只作过渡 |
| 8.1 及更早(含 7.x、5.x) | 全部 | 无官方补丁 | 官方仅提供 7.4/8.0/8.1 源码回植,需自行编译 |
三条补充判断:
第一,版本号必须精确到小版本。「我是 8.4」这句话没有意义,8.4.26 和 8.4.27 在这里是生与死的区别。判断依据是第三段数字,不是第一段。
第二,8.1 及以下没有官方补丁。PHP 项目早就停止为这些版本提供安全更新了。官方只给 7.4、8.0、8.1 发了最小源码回植补丁,供从源码编译的团队自己打。发行版(Debian、Ubuntu、RHEL 及其衍生版)和部分 LTS 商业支持方可能有回植包,但志愿者维护的 Remi 仓库在这次事件中一度没跟上。跑 8.1 及以下的机器,短期只能靠网络隔离加监控撑着,中期必须迁移。
第三,容器和面板是重灾区。一台机器上同时装了三四个 PHP 分支(宝塔默认目录 /www/server/php/ 下 74/80/81/82/83/84 各一份)、几个跑着不同镜像的容器,这种情况非常常见。你修了站点正在用的那个,另一套还在跑的旧版本照样是入口——它可能没绑域名,但只要 80/443 能被访问到,就一样能被利用。
三、为什么说升级是唯一完整方案
这是本文的核心判断,值得单独一节讲清楚:临时配置能降低暴露风险,但它不修漏洞。两者不是「效果差一点」和「效果好一点」的关系,是「堵一条路」和「把洞填上」的关系。
理由很实在。两个 CVE 都住在解释器的内存管理和请求生命周期代码里。你调 memory_limit、关 session.upload_progress、限制请求体大小,做的都是让攻击者够不到那条已知的触发路径。漏洞本身还在原地:那套损坏状态下的关闭流程没改,那个悬空指针也还在。一旦有人换一条路径(换 SAPI、换上传方式、换触发顺序),缓解措施就绕过去了。
| 维度 | 升级到官方安全版本 | 临时配置缓解 |
|---|---|---|
| 是否修复根因 | 是,改的是解释器代码 | 否,只改运行参数 |
| 对已公开利用链 | 完全免疫 | 能挡住当前已知的那几条 |
| 对未公开变体 | 一并解决 | 大概率无效 |
| 持续成本 | 一次性,之后不用管 | 长期挂着,还可能被业务覆盖掉 |
| 副作用 | 需验证扩展兼容性 | 关进度条、限内存、可能影响大文件上传 |
| 合规/审计 | 可作为修复证据 | 通常不被认可为「已完成修复」 |
还有一个反直觉的点,值得特别提醒:临时缓解措施不一定让系统更安全,用错方向甚至更危险。
比如官方建议针对 109618 把 memory_limit 设成 -1(不限制)。听起来疯狂,逻辑是:既然攻击者要靠「控制内存耗尽点」来触发那条路径,那把内存限成无限,他就没法控制了。但代价是——失控请求可以真的吃满整台服务器,一次攻击变成一次拒绝服务。所以官方同时说,要用容器或 cgroup 的内存限制作为可用性边界,而且绝不能用 RLIMIT_AS / RLIMIT_DATA 来限,因为那会以 mmap 失败的形式正好触发漏洞路径。
另外还有一条容易被忽略的残留风险:session.upload_progress 的文件名未验证缺陷,在 8.5.12 里依然存在(官方那个修复方案还没合入主线)。它单独不能命令执行,但如果你的应用里存在文件包含类漏洞,两者叠加就能打通。所以即使升级完成,只要你的站点不用上传进度条,把 session.upload_progress 关掉仍然是加分项,不是多余动作。
四、动手第一步:先确认你到底在跑哪个 PHP
很多人卡在这一步,而且错得很隐蔽:敲 php -v 看到的版本,和你网站实际运行的版本,很可能不是同一个东西。
原因很简单。php -v 跑的是命令行(CLI)二进制,通常是 /usr/bin/php 或 /www/server/php/84/bin/php。而网站请求走的是 PHP-FPM 的 socket、Apache 的 mod_php 模块、或者 php-cgi,那是另一个二进制、另一份 php.ini、另一次编译。同一台机器上 CLI 是 8.4.27、FPM 还是 8.3.20 的情况,一点都不稀奇。
尤其这几种环境最容易看错:
· 宝塔、cPanel、Plesk 这类面板:面板首页显示的是「已安装版本」,站点绑定的是某个具体分支的 socket,两者不一定一致;
· 多分支并存:/www/server/php/ 下面躺着 74 到 84 一堆目录,改了一个不等于改全部;
· 容器:宿主机 php -v 是新的,容器里镜像还是旧的;
· Windows:php.exe 和 php-cgi.exe 是两套,IIS 用的往往是后者。
可靠的确认顺序是从 Web 侧往回收,而不是从命令行往外推:
# 1. 看站点到底转发给哪个 PHP(nginx 视角)
grep -rE "fastcgi_pass|enable-php" /etc/nginx/ /www/server/panel/vhost/nginx/ 2>/dev/null | grep -v "#"
# 2. 看 FPM 池里配的 user/group 和监听方式,确认是哪个分支
ps -ef | grep "[p]hp-fpm"
# 3. Web 侧真实版本(在站点根目录放一个临时探针,看完立刻删)
echo '<?php echo PHP_VERSION, " / ", PHP_SAPI;' > /www/wwwroot/站点目录/_v.php
curl -s https://你的域名/_v.php
rm -f /www/wwwroot/站点目录/_v.php
第 3 步是最硬的证据,因为它就是攻击者会看到的那条路径。PHP_SAPI 会明确告诉你它是 fpm-fcgi、apache2handler 还是 cli-server,配合版本号一起看,才有意义。
顺手说个真实例子。我这台跑站点的机器上,php -v 返回 PHP 8.4.27 (cli) (built: Oct 22 2026 02:38:50) (NTS),二进制文件 /www/server/php/84/sbin/php-fpm 的修改时间是 10 月 22 日 03:27——漏洞公布第二天就重编了包。但「二进制是新的」不等于「运行的是新的」,下一节讲为什么。
五、四个修复版本怎么选:别为了修漏洞顺手跨大版本
官方给了四个版本,任选其一,不必和原有分支一一对应。这就带来一个诱惑:既然都能修,干脆一步到位升到最新的 8.5.12 得了。我的建议是别这么干,除非你本来就计划升。
| 现状 | 建议目标 | 理由 |
|---|---|---|
| 8.4.x | 8.4.27 | 同分支打补丁,扩展零改动,风险最低 |
| 8.3.x | 8.3.36 | 同上;8.3 仍在维护期,不急迁 |
| 8.2.x | 8.2.35 或直接 8.4.27 | 8.2 年底 EOL,不如现在就规划迁到 8.4 |
| 8.1 及以下 | 8.3.36 / 8.4.27 | 没有官方补丁,只能迁移;迁前先在测试环境跑一遍 |
| 新装环境 | 8.4.27 | 支持周期长、生态兼容性好,8.5 生态还在跟进 |
跨大版本升级的真实成本,不在 PHP 本身,在扩展和代码:
· 编译型扩展(imagick、redis、swoole、gd 的某些特性)必须对目标版本重新编译,ABI 对不上就是「模块未加载」,表现是白屏或 500;
· 一些老函数在 8.4 里被废弃(隐式可空参数、动态属性相关的告警),开了 E_ALL 之后页面会刷满 deprecated 提示;
· 框架和 CMS 的兼容性。WordPress 主线和主流主题对 8.4 基本没问题,但一些老插件不一定。
所以顺序应该是:先在同分支打补丁把洞堵上(1 小时内可完成),把跨版本迁移排进计划单独做。这两件事混在一起做,最容易出「修好了漏洞,网站也打不开了」的场面。
六、升级实操:面板、源码、容器三条路
不同部署方式的操作差别很大,分开说。
路线一:宝塔 / cPanel 这类面板。软件商店里找到对应 PHP 分支,点「设置」看当前版本,然后切换或更新到补丁版本。要注意两点:一是编译参数要保留原来的(安装时勾选过的扩展,重装时别漏),否则 GD、imagick、fileinfo 会跟着丢;二是面板的版本号有时是缓存的,装完要点「重载配置」或直接看 bin/php -v 确认。
路线二:源码编译。关键是别丢配置参数。先记住原来的:
# 备份原编译参数(升级后必然要用)
php -i | grep "Configure Command"
# 下载对应版本源码,用原参数重新 configure
./configure --prefix=/www/server/php/84 你原来的全部参数
make -j$(nproc) && make install
路线三:容器。这里有个经典的错误:只重启容器,不重建镜像。容器的文件系统来自镜像,你重启多少次,里面跑的还是那个旧镜像里的旧 PHP。必须拉新 tag、rebuild、再滚动替换:
docker pull php:8.4.27-fpm
# 改了 Dockerfile 里的 FROM 之后
docker compose build --no-cache php
docker compose up -d php
如果上游不给你用的分支出补丁(比如还在跑 7.4 的镜像),有一条实用的退路:第三方安全厂商发布了把补丁回植到旧版本的镜像,tag 名保持原版本号,例如 public.ecr.aws/aikido-dev/php:8.4.27-fpm。这比自己打补丁省事得多,但用之前要确认镜像来源可信。
路线四:发行版仓库。Debian/Ubuntu/RHEL 系的补丁通常已经在源里了,但要确认它真的包含这次修复:
apt changelog php8.3-cli | grep -E "CVE-2026-109618|CVE-2026-109619"
rpm -q php-cli --changelog | grep -E "CVE-2026-109618|CVE-2026-109619"
有输出才说明包里有补丁。没输出就说明你装的是「版本号没变但没有补丁」的旧包,得换源或者从源码编。
七、重启这一步最容易漏:reload 不等于重启
这是整篇文章里最需要划重点的一节。升级了二进制,不等于运行中的进程换成了新代码。已经加载进内存的程序、正在跑的 master 进程,不会因为你替换了磁盘上的文件而自动更新。
更麻烦的是,很多人以为「我 reload 过配置了,应该生效了吧」。不是。
PHP-FPM 的 reload(SIGUSR2)是优雅重载:master 重新读配置、重新 fork worker,但 master 进程本身还是原来那个二进制。新 fork 出来的 worker 是从 master 继承的内存映像,所以它们跑的仍是旧版 PHP。要真正换血,必须让 master 退出、重新拉起:
# FPM:用 restart,不要只用 reload
systemctl restart php8.4-fpm # 发行版
/etc/init.d/php-fpm-84 restart # 宝塔
service php-fpm restart # 通用
# Apache:mod_php 用 reload 也不换模块,必须 restart
systemctl restart apache2 # Debian/Ubuntu
systemctl restart httpd # RHEL/CentOS
按部署方式,要重启的东西不一样,这张表可以直接对着做:
| 运行方式 | 必须做的动作 | 容易漏的点 |
|---|---|---|
| PHP-FPM | restart master 进程 | 只 reload 配置,master 还是旧二进制 |
| Apache mod_php | restart apache | graceful reload 不重新加载 DSO 模块 |
| Docker / K8s | 重建镜像再替换容器 | 只 restart 容器,镜像没换等于没修 |
| LiteSpeed | 重启 lsws(lsphp 也要重启) | 只重启 lsphp 不够,父进程仍持有旧句柄 |
| FrankenPHP / Swoole / RoadRunner | 重启常驻 worker | 常驻进程不重启,代码和运行时都不换 |
| CLI 定时任务 | 无需动作(每次新起进程) | 但 cron 里写死的 php 路径要确认指向新版本 |
OPcache 要单独处理。开了 OPcache 的实例,编译过的字节码存在共享内存里,不跟着进程重启自动清的可能性是存在的(尤其多 pool 共享同一段缓存时)。三条命令任选其一,建议第二个:
systemctl reload php8.4-fpm # 发 SIGUSR2,OPcache 会重置
curl -s "https://你的域名/opcache-reset.php" # 页面里调 opcache_reset()
php -r "opcache_reset();" # 仅当 CLI 与 FPM 共享同一 opcache 段时有效
还要提醒一句:如果 nginx 前面还有一层缓存、CDN 或者反向代理,PHP 层面的修复不会被它们感知。修完之后记得清一遍页面缓存,否则你验证时看到的还是旧页面,误以为没修好。
八、重启完必须核对:三层验证才算数
「我重启了」和「重启生效了」之间还差一次核对。而且核对不能只看一个地方,要分三层,从外到内逐层确认。
第一层:CLI 版本。最简单,但也最不代表网站。看版本号 + 编译日期:
php -v
# PHP 8.4.27 (cli) (built: Oct 22 2026 02:38:50) (NTS)
# ^^^^^^^ 第三段 ≥ 27 才算过
第二层:Web SAPI 版本。放临时探针,或者用现成的方式让站点输出 PHP_VERSION。这一步看到的是攻击者会看到的答案,必须 ≥ 8.4.27 / 8.3.36 / 8.2.35 / 8.5.12 对应分支。看完把探针删掉——留着的探针本身就是信息泄露。
第三层:进程真身。这一层最容易被跳过,却是判断「到底有没有换血」的唯一硬证据。核心是比对二进制的修改时间和进程的启动时间:
# 二进制什么时候编的
stat -c "%y %n" /www/server/php/84/sbin/php-fpm
# 2026-10-22 03:27:39 /www/server/php/84/sbin/php-fpm
# master 进程什么时候起的
ps -o pid,lstart,cmd -C php-fpm | head -3
# 1071 Thu Oct 29 14:03:31 2026 php-fpm: master process (...)
# 这个 PID 实际映射到哪个文件(最终确认)
ls -l /proc/1071/exe
🔧 我整理的软件库
这些年折腾服务器、虚拟化和开发环境,装过的系统镜像、运维工具、开发套件、数据库与中间件,我都归档在自建的文件库里,持续补充新版本。没有弹窗广告,不强制注册,需要什么直接取。
常用工具随手可取 · 不定期补档
判断规则一句话:进程启动时间必须晚于二进制文件的修改时间。如果正在跑的 master 是 10 月 20 日启动的,而 php-fpm 二进制是 10 月 22 日才换的,那它跑的就是旧代码——无论 php -v 显示什么,无论面板上写着什么版本。
我这台机器上的结果正好是反过来的:二进制 10-22 03:27 编好,master 10-29 14:03 启动,/proc/1071/exe 指向 /www/server/php/84/sbin/php-fpm,顺序对得上,说明升级之后确实重启过 master,不是只替换了文件。
验证项汇总成一张表,逐条打勾:
| 验证项 | 命令 | 期望结果 |
|---|---|---|
| CLI 版本 | php -v | ≥ 8.4.27 / 8.3.36 / 8.2.35 / 8.5.12 |
| Web SAPI 版本 | 临时探针 + curl | 和 CLI 一致,SAPI 为 fpm-fcgi / apache2handler |
| 进程启动时间 | ps -o lstart -C php-fpm | 晚于二进制 mtime |
| 进程真实二进制 | ls -l /proc/<PID>/exe | 指向新版本路径,不是 (deleted) 旧文件 |
| 配置未被覆盖 | 查 pool / .user.ini / .htaccess | 无 pool 级 php_admin_value 回置 memory_limit |
| 站点可用 | curl 首页 + 一个功能页 | 200,无白屏、无 500 |
关于「配置未被覆盖」,展开一句:PHP 的参数优先级是 php_admin_value(FPM pool / Apache 配置)> 目录级 .user.ini > php.ini > ini_set()。你按官方建议在 pool 里设了 memory_limit = -1,结果某个目录下的 .user.ini 又把它改回 128M,那等于白设。这一点在恢复临时配置时特别容易踩:改回去的时候只改了 php.ini,pool 里的覆盖还在。
九、补丁只挡新攻击:已经中过招怎么查
这一节必须写,因为很多人把它当成「可选项」,跳过就收工了。打补丁只能阻断未来的攻击,清不掉已经进去的东西。一个已经拿到命令执行权限的攻击者,在你去打补丁的这几个小时里,完全可能已经种了 WebShell、加了管理员账号、改了某个 PHP 文件的落地页。
排查从日志开始。这次漏洞的请求特征相当明显,官方和各家安全厂商都给了检测指纹:
| 特征 | 含义 | 正常业务会出现吗 |
|---|---|---|
| POST 表单字段名就叫 PHPSESSID | 会话固定攻击特征 | 不会 |
| 出现 PHP_SESSION_UPLOAD_PROGRESS 字段 | 直指上传进度入口 | 只有真用进度条的页面才有 |
| 单请求上千个表单字段 / 单字段超 8MB | 内存耗尽攻击特征 | 不会 |
| Allowed memory size … exhausted … in Unknown on line 0 | 请求启动阶段 OOM 的典型症状 | 不会,往往意味着已经被打过 |
| worker 段错误/SIGABRT 增多、500 突增 | UAF 触发的崩溃 | 偶发可能有,成规模就不正常 |
| Set-Cookie: PHPSESSID 异常超长或含二进制 | 内存信息泄露指纹 | 不会 |
几条直接能跑的命令:
# 1. 访问日志里翻攻击特征(有请求体记录的日志才看得到字段名)
grep -E "PHP_SESSION_UPLOAD_PROGRESS|PHPSESSID=" /var/log/nginx/access.log | head
# 2. PHP 错误日志里翻 OOM 指纹
grep -rE "Allowed memory size .* exhausted .* in Unknown on line 0" /var/log/php* /www/wwwlogs/*.log | head
# 3. 找近期被改过的 PHP 文件(排除缓存和上传目录)
find /www/wwwroot/站点目录 -name "*.php" -mtime -14 -not -path "*/cache/*" -ls
# 4. 看 PHP 进程有没有异常子进程(正常 FPM worker 不该 fork 出 shell)
ps -ef --forest | grep -A2 "php-fpm: pool"
主机侧还要看四样东西:WebShell 文件(常见于 uploads、缓存、图片目录里的小马)、新增的管理员账号、异常外联(PHP 进程往外连非常规端口)、以及 .htaccess / index.php 被追加的跳转代码。
处置顺序不要乱:先隔离(断外网或临时下线),再取证(打包日志、导出站点文件快照),然后轮换所有可能泄露的凭据——数据库密码、API Key、第三方平台 token 都要换,因为代码执行意味着攻击者能读到 PHP 进程有权限读的一切,包括 wp-config.php / .env 里的明文密码。最后才是清理和加固。
如果站点已经出现异常、又不确定从哪一步开始查,可以直接找我聊:微信 / 手机 17712677007,把现象说清楚(哪个页面报什么错、日志里有没有上面那些特征、大概什么时候开始的),先判断是「没修好」还是「已经中招」,这两种情况的处理顺序完全不一样。
十、实在不能马上升级:临时缓解怎么写才算用对
有些环境确实升不动——老系统绑死在 8.1、换了 PHP 版本就跑不起来、或者升级要等业务窗口。这时候只能用临时措施顶几天。但必须清楚:这是降暴露,不是修复,而且写错位置等于没写。
最重要的一条铁规:这些设置必须落在「请求解析之前」生效的位置,也就是 FPM pool 配置的 php_admin_value,或者 Apache 的 php_admin_value 指令。官方明确说过,不能依赖 ini_set()、.user.ini 在应用里改——因为等应用代码跑起来,漏洞的触发点早就过去了。这一条是很多人配了却没效果的根本原因。
| 措施 | 写在哪 | 挡住什么 | 副作用 |
|---|---|---|---|
| session.upload_progress.enabled = 0 | FPM pool / Apache | 直接关掉上传进度入口 | 上传进度条失效 |
| memory_limit = -1 | FPM pool / Apache | 攻击者无法控制内存耗尽点 | 失控请求可能吃满服务器,须配 cgroup |
| fatal_error_backtraces = 0 | FPM pool / Apache | 仅 PHP 8.5,阻断致命错误回溯路径 | 排障时可读信息变少 |
| pm.max_requests = 1 | FPM pool | 每 worker 处理 1 个请求就退出,限制跨请求残留 | 性能大幅下降,只能应急 |
| post_max_size 降到 8M / 32M | php.ini | 压住请求体体积 | 大文件上传失败,upload_max_filesize 要更小 |
| Web 层限请求体/字段数/分段数 | nginx / Apache / 反代 | 在到达 PHP 之前拦掉 | 需要调阈值避免误伤正常表单 |
| 无需上传的接口拒绝 multipart/form-data | nginx / 反向代理 | 连入口都不给 | 要逐个接口核对,容易误伤 |
绝对不要做的两件事:一是用 RLIMIT_AS / RLIMIT_DATA 限制内存——官方专门警告过,这会让内存分配以 mmap 失败的形式失败,正好把请求走进漏洞那条路径;二是把 display_errors 打开着「方便排查」——错误信息里可能带上内存地址和运行路径,等于给攻击者送情报。
另外补两条低成本但有效的:session.use_strict_mode = On(拒收未初始化的会话 ID,抬高会话固定类利用的门槛)、生产环境 display_errors = Off 且 error_reporting 收敛。这两条不改变漏洞,但能显著提高现成 PoC 的失败率,在「还没升完级」的窗口期很值。
十一、几个高频疑问
问:升级后网站白屏或 500,是不是升错了?
多数情况下不是版本错,是扩展没跟上。先看错误日志里有没有 Unable to load dynamic library 或 Module ... is already loaded,那就是编译型扩展的 ABI 不匹配。处理办法是对新版本重装扩展(imagick、redis、swoole 这类),或者临时把不关键的扩展从 ini 里注释掉,先把站点跑起来再补。
问:我重启了 nginx,算不算重启 PHP?
不算。nginx 只负责把请求转发给 PHP-FPM 或 Apache,它自己不含 PHP 解释器。重启 nginx 对 PHP 版本没有任何影响。
问:面板显示已经是最新版了,还要做什么?
看数字。面板的「最新版」有时指该分支的最新编译包,有时是缓存值。必须确认具体小版本 ≥ 8.4.27 / 8.3.36 / 8.2.35 / 8.5.12,并且用第七节的三层验证确认进程真的换了。
问:还在跑 PHP 7.4 / 8.1,官方没补丁怎么办?
三条路,按稳妥程度排序:迁移到 8.3.36 或 8.4.27(正解,但要测试);用发行版或 LTS 厂商的回植包;用官方给 7.4/8.0/8.1 的最小源码回植补丁自行编译。哪条都做不了,就把它从公网摘下来,只在内网访问,同时加强监控。
问:Docker 里只 restart 容器行不行?
不行。容器重启只是重新跑镜像里的旧文件,PHP 还是旧的。必须拉新镜像并重建容器。
问:修完之后临时配置要不要恢复?
分两种。memory_limit 那种为了缓解而改的,确认新版本跑起来后要改回原来的值,否则长期不限制内存是隐患;session.upload_progress.enabled = 0 建议保持关闭——它是很少用的遗留功能,而且文件名验证那个缺陷在 8.5.12 里还在,关着没有坏处。
问:用虚拟主机,没法自己升怎么办?
直接找服务商要书面确认:当前 PHP 版本号是多少、是否 ≥ 本次修复版本。口头说「已经处理了」不算,要具体版本号。
十二、一页速查清单
把整篇压缩成一张表,照着打勾就行。顺序不要调换:
| # | 动作 | 判定标准 |
|---|---|---|
| 1 | 确认 Web SAPI 实际版本(不是只敲 php -v) | 拿到明确小版本号 + SAPI 类型 |
| 2 | 清点全部 PHP 实例(分支、容器、面板、Windows 双二进制) | 清单里没有遗漏在跑的旧版本 |
| 3 | 升级到 ≥ 8.4.27 / 8.3.36 / 8.2.35 / 8.5.12 | 第三段版本号达标 |
| 4 | restart(不是 reload)所有 PHP 进程 | master 进程 PID 变了 |
| 5 | 清 OPcache | 缓存重置时间刷新 |
| 6 | 容器重建镜像(不是 restart 容器) | 容器内 php -v 达标 |
| 7 | 三层核对版本 | CLI、Web、进程真身三者一致 |
| 8 | 查配置覆盖(pool / .user.ini / .htaccess) | 没有更低优先级的配置把参数改回去 |
| 9 | 关闭 session.upload_progress | FPM pool 里生效,重启后仍在 |
| 10 | 查入侵痕迹(日志 + 文件 + 进程 + 账号) | 六类特征全部为 0;有命中就按处置顺序走 |
| 11 | 清页面缓存 / CDN | 线上抓到的是新响应 |
最后说一句判断标准,也是这篇文章想表达的核心:「我升级了」不是一个可以自我确认的状态。它需要三个证据——Web 侧版本号达标、进程启动时间晚于二进制时间、日志里没有异常特征。三样都齐了才算修完。
这次事件的教训其实跟 PHP 本身关系不大:漏洞发现的速度已经超过了人工响应的速度,AI 参与漏洞研究之后,从披露到被利用的窗口可能只有几十个小时。所以「确认版本 → 打补丁 → 重启 → 核对」这个动作本身要变成肌肉记忆,而不是每次出事才临时查文档。你可以在评论区说说自己机器上是怎么处理的,或者遇到了哪个坑。
✍️ 关于作者
IT 峰哥,互联网技术从业者。日常折腾服务器、虚拟化、数据库与企业级应用,关注开源系统的二次开发与基础设施落地。这里记录的是踩过的坑和验证过跑得通的方案,不是转述文档。
交流:微信 / 手机 17712677007(注明来意,看到会回)
文中方案如有疏漏,欢迎在评论区指正 · 技术问题欢迎交流