工作

一次 PHP 安全补丁的 20 分钟:从编译到验证

2026 年 10 月 22 日,阿里云发来一条安全通告。这篇文章记录了从收到告警到确认修复的完整过程。

收到告警

下午三点多,手机弹出一条阿里云短信:PHP 曝出「php2shell」远程代码执行漏洞,编号 CVE-2026-109618 与 CVE-2026-109619,建议尽快升级。

这类安全短信我通常会先做两件事:核实真伪,确认自己的版本。

第一步:先别急着操作,核实公告

短信里的 CVE 编号和漏洞名听起来都很正式,但也不能直接照着短信里的链接点。我打开了几个独立来源交叉验证:

  • IPA(日本情报处理推进机构)在 10 月 22 日发布了正式安全通告
  • 斯洛文尼亚 SI-CERT、北约合作网络防御中心 NCSC 都有对应条目
  • 漏洞由 Wiz 研究团队发现,官方修复版本明确:8.2.35 / 8.3.36 / 8.4.27 / 8.5.12

漏洞本身是两个问题叠加:一个是 OOM 内存不足时的错误处理,另一个是请求处理中的状态残留。单独看都不算特别严重,组合起来却能实现远程代码执行(RCE),CVSS 评分 9.2。

结论:真实漏洞,需要处理。

第二步:查自己的版本

/www/server/php/82/bin/php -v
# PHP 8.2.28 → 低于 8.2.35,在受影响范围内

服务器是 2 核 2G 的阿里云 ECS,跑着宝塔面板、nginx、PHP-FPM 和 MySQL,托管着我的博客。这种情况最常见的处理方式是:直接在宝塔面板里点升级。

但我先做了一件事。

第三步:没有备份,不上服务器

交接文档里一直写着「备份目录为空」。在改动任何运行环境之前,我先把三样东西完整备份:

# 1. 数据库
mysqldump -u user -p --single-transaction --quick liuby_top | gzip > liuby_top.sql.gz

# 2. 网站文件
tar czf wordpress.tar.gz -C /www/wwwroot/liuby.top wordpress

# 3. PHP 二进制与配置
tar czf php82.tar.gz -C /www/server/php 82

其中 PHP 那 19 MB 是最关键的——如果升级失败,可以直接解压回去恢复原版本。

这一步的意义不在于“一定会出事”,而在于出事的时候你有退路。升级失败的代价是几分钟回滚,而不是重建服务器。

第四步:编译还是用预编译包

机房服务器列
机房里的服务器,安静地运行时没人会想起它

宝塔提供的 PHP 升级通常有两种形式:面板一键预编译包,或者自己拉源码编译。

我先探测了宝塔镜像站:

curl -I https://download.bt.cn/src/php-8.2.35.tar.gz
# HTTP 200 —— 官方源码存在

镜像站有源码,那就走源码编译。好处是可以完全复刻原来的编译参数,扩展一个不少。

关键动作是:升级前先记录当前的扩展清单和 configure 参数。

php -m > before.txt
php -i | grep "Configure Command"

然后使用完全相同的参数编译新版本:

./configure --prefix=/www/server/php/82 \
  --enable-fpm --with-fpm-user=www --with-fpm-group=www \
  --enable-mysqlnd --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd \
  --enable-bcmath --enable-mbstring --enable-intl --enable-gd \
  --with-curl --with-openssl --with-sodium --with-webp \
  --enable-opcache ... # 其余参数照抄原版
make -j2

2 核机器上,configure 大约 4 分钟,make 大约 13 分钟。期间服务器负载会升高,但php-fpm 还在跑旧版本,网站不受影响。

第五步:装之前先验证

编译完成不等于可以直接装。我先用编译出来的二进制做了一次体检:

./sapi/cli/php -v
# PHP 8.2.35 ✓

./sapi/cli/php -m | diff - before.txt
# 无差异 ✓

确认版本对、扩展齐,才开始替换。

第六步:替换,然后立刻验证

/etc/init.d/php-fpm-82 stop     # 停机开始
make install                     # 替换二进制
/etc/init.d/php-fpm-82 start     # 停机结束

这次停机大约 2 分钟。

替换后要验证的东西比想象中多:

| 检查项 | 命令 | 结果 ||—|—|—|| CLI 版本 | php -v | 8.2.35 || 配置语法 | php-fpm -t | success || 进程状态 | ps aux \| grep php-fpm | 正常运行 || 首页 | curl -I https://站点 | 200 || 文章页 | curl -I https://站点/文章 | 200 || 搜索 | curl "https://站点/?s=test" | 200 || 错误日志 | tail error.log | 无新增错误 |

一个容易被忽略的点:扩展文件会被覆盖

make install 会重建扩展目录。如果你的 PHP 有单独编译的扩展(比如 opcache.so 是独立文件),装完之后要检查它们还在不在。

我的检查方式很朴素:

ls -la /www/server/php/82/lib/php/extensions/*/

发现 zip.so 还在,opcache.so 被新版本覆盖成了新的——这个是正常的,因为 opcache 是官方自带扩展。

事后总结

这次升级总共花了约 20 分钟,其中 17 分钟在编译,真正影响线上服务的只有 2 分钟。

几点体会:

安全通告要核实,但不要拖。 大范围曝光的漏洞,补丁发布后几小时内就会有扫描器开始批量探测。

备份是升级的一部分,不是前置条件。 很多人卡在“没备份所以不敢升”,然后一直不敢升。

用完全相同的编译参数。 这是避免“升级后某个功能突然不可用”最有效的方法。

验证要逐个检查,不能只看首页。 首页 200 不代表文章页、搜索、登录都正常。

最后提醒一句:PHP 8.1 及以前已经停止维护,这次漏洞不会给它们发补丁。如果你还在用更老的版本,问题不是“要不要升级”,而是“什么时候必须升级”。

发表评论