澄成 HELP
宝塔 · PHP安全更新 · 实际处置记录

宝塔PHP高危漏洞修复实录:8.2.35升级与FPM验收

本页记录澄成官网一次实际的宝塔环境PHP安全处置:确认运行中的PHP版本、查站点绑定、备份、校验官方源码、在隔离目录编译、验证后替换、重启全部FPM工作进程,最后分别检查动态业务与公网访问。命令中的目录是本次环境示例,不是所有宝塔服务器的固定配置。

1. 先弄清这次是什么漏洞

2026年10月21日,PHP项目发布两处严重远程代码执行漏洞的修复:CVE-2026-109618和CVE-2026-109619,分别涉及内存耗尽处理时的状态损坏、跨请求的陈旧内部状态。不要把它们与以前的Windows CGI参数注入漏洞混为一谈。

官方明确PHP-FPM也在受影响范围。换成Nginx、前面有CDN,或者首页是静态HTML,都不能单独证明安全;登录、接口、后台等请求仍可能进入PHP。我们没有用攻击脚本尝试让线上进程崩溃,而是根据实际版本、构建来源和运行路径核对修复状态。

修复版本为PHP 8.2.35、8.3.36、8.4.27、8.5.12。发行版可能回移植补丁,因此其他机器不能只按显示版本号判断,应核对应的软件包安全公告。本次是宝塔独立编译版,不属于系统RPM包。

2. 本次环境实际发现了什么

因此本次需要升级真正承载业务的PHP 8.2运行时,同时处理无人使用但仍运行的旧实例。只在SSH里看到一个较新php -v,或者只升级系统包,都可能漏掉网站真正使用的解释器。

一行启动工具与适用范围

进入服务器安全专题,查看一行启动命令和完整脚本

我们把本次PHP8.2处置做成了可检查的启动工具:先下载到随机临时文件,核固定SHA256,再以root启动;不使用不校验来源的curl管道直接执行。脚本默认--check只读,--apply才会备份、构建和切换;同一次只能运行一个修复任务。

适用边界是Linux、宝塔Nginx、现有源码版/www/server/php/82、相应SysV脚本和已具备的编译依赖。新编译路径要求GCC9+、至少750MiB可用内存;旧CentOS7工具链或低可用内存会停止,不能把它说成任意宝塔都可一键修好。它不会自动把7.4或8.3迁到8.2,不更新所有PHP分支、不自动安装系统依赖;如果另有活动PHP实例,先退出,要求单独核依赖和处置。本文的5.6停用步骤是我们核完站点绑定后的人工操作,不藏在通用一键脚本里。

这是一行“启动完整修复流程”,不是一秒完成。编译时间取决于CPU、内存和业务负载;切换会重启FPM,可能短暂影响动态请求。请先安排维护窗口并保留SSH连接,脚本的运行时验收不替代各业务流程验收。

第二台CentOS 7:为什么不能原样复制第一台的构建过程

第二台服务器使用CentOS 7与glibc 2.17,也运行PHP8.2.28,同时保留PHP7.4.33进程。Nginx配置中的几个站点虽然包含enable-php-74.conf,实际请求走Node反向代理或静态文件;核对应目录没有PHP文件,旧FPM在108天内累计连接仅19次。我们备份后停用7.4并取消自启,原静态/代理入口200或301、实际PHP82管理登录页200均保持。这里不能只看到“绑定7.4”就强行升级应用,也不能只看进程少就贸然停止。

首轮编译出现过高负载、SSH及动态探测超时,最终在OpenSSL扩展处报错退出,未替换生产运行时。排查发现:旧PHP实际链接的是已安装的OpenSSL1.1.1w,而默认pkg-config返回系统1.0.2k,头文件/库选择与原运行时不同。只复用php-config --configure-options会遗漏这类构建环境。

后续明确选择原有OpenSSL1.1.1w的pkg-config目录。该机原FPM没有加载OPcache,也未启用JIT;本次保留字节码缓存模块,但构建时禁用JIT,减少老编译器的内存需求。没有把第一台系统生成的二进制直接复制过来。

export PKG_CONFIG_PATH=/usr/local/openssl111/lib/pkgconfig:/usr/local/onig/lib/pkgconfig:/usr/local/libsodium/lib/pkgconfig:
pkg-config --modversion openssl
# 在复用原configure选项的基础上,仅此台额外加入:
# --disable-opcache-jit

为保护正在运行的业务,“仅编译”阶段放入单独资源组,内存上限700MB、最多一个CPU核心、Nice10。CentOS 7的systemd219使用MemoryLimit;不要照抄新系统才支持的属性。原PHP进程和后续服务重启不放进这个临时编译资源组,以免资源组退出时连业务进程也被回收。

本机使用的完整构建脚本如下。先完成下文环境核对、备份和官方源码校验,再按实际路径准备脚本;其中OpenSSL、oniguruma和libsodium路径都来自本机核验,不能盲目移植:

cat > /root/php-security-20261022/build-safe.sh <<'BUILD'
#!/bin/bash
set -eu
TASK=/root/php-security-20261022
cd "$TASK"
mkdir -p source-safe
if test ! -d source-safe/php-8.2.35; then
  tar -xf php-8.2.35.tar.xz -C source-safe
fi
cd source-safe/php-8.2.35
export CC=/usr/bin/gcc CXX=/usr/bin/g++
export PKG_CONFIG_PATH=/usr/local/openssl111/lib/pkgconfig:/usr/local/onig/lib/pkgconfig:/usr/local/libsodium/lib/pkgconfig:
pkg-config --modversion openssl
python3 - <<'PY'
import shlex, subprocess
args = shlex.split(subprocess.check_output([
    '/www/server/php/82/bin/php-config', '--configure-options'
]).decode())
subprocess.check_call(['./configure'] + args + ['--disable-opcache-jit'])
PY
make -j1
make install INSTALL_ROOT="$TASK/stage"
"$TASK/stage/www/server/php/82/bin/php" -n -v
"$TASK/stage/www/server/php/82/sbin/php-fpm" -v
date -Is > "$TASK/build-success"
BUILD
chmod 700 /root/php-security-20261022/build-safe.sh

随后启动“只编译、不切换”的资源组。实际操作中我们把该脚本标准输出与错误输出重定向到私有build-safe.log,便于SSH断开后继续检查;systemd journal也可查看单元状态。下面展示资源控制参数:

systemd-run --unit=php82-security-build47 \
  --property=MemoryLimit=700M --property=CPUQuota=100% \
  --property=CPUShares=128 --property=Nice=10 \
  /bin/bash /root/php-security-20261022/build-safe.sh
systemctl show php82-security-build47 \
  -p ActiveState -p MemoryCurrent -p MemoryLimit -p CPUQuotaPerSecUSec

build-safe.sh只做前文的独立源码配置、make、暂存安装与版本检查,不启动或重启PHP服务。编译资源触顶时应允许构建失败,再调整方法,不提高业务PHP的内存上限来掩盖问题。针对这次经验,公开工具已经加入旧编译器和低可用内存门槛,不再让类似环境直接进入无人值守的在线编译。

实际结果:受限单任务构建完成后,先在暂存目录验证PHP8.2.35和11项基础功能,再通过独立SSH会话运行修复脚本,未在构建单元内启动生产服务。切换后Unix socket实际FPM返回8.2.35,9个业务模块均加载;主进程映射哈希与新FPM一致,php.ini、php-cli.ini、php-fpm.conf与备份逐字节相同。管理登录页返回200且含登录标题;其余Node/静态路由保持原200或301。重复执行工具识别已修补版本,不再次安装或重启。旧7.4实例停用并取消自启,文件保留,不能写成7.4已经升级。没有进行数据库写入、真实客户表单、完整上传或计划任务验收。

3. 第一步:查CLI、FPM、进程与站点绑定

以下命令是只读检查。先确认自己的实际路径,再执行;不要照抄其他机器的版本目录。

command -v php
php -v
/www/server/php/82/bin/php -v
/www/server/php/82/sbin/php-fpm -v
/www/server/php/82/bin/php --ini
/www/server/php/82/sbin/php-fpm -i \
  | grep -E '^(PHP Version|Loaded Configuration|memory_limit|post_max_size|upload_max_filesize|session.upload_progress.enabled)'
ps -eo pid,args | grep '[p]hp-fpm: master process'

再读取运行中主进程实际使用的二进制。把PID换成刚查出的主进程号:

readlink /proc/PID/exe
sha256sum /proc/PID/exe

检查生效配置,不能只看宝塔网站列表里的显示标签:

/www/server/nginx/sbin/nginx -T 2>&1 \
  | grep -E 'server_name|enable-php|fastcgi_pass|php-cgi-56.sock|php-cgi-82.sock'
rpm -qf /www/server/php/82/bin/php

本次主站与同机其他动态站均指向php-cgi-82.sock。php-cgi-56.sock只出现在状态查看路由,没有业务站绑定。完整Nginx配置可能带私有地址或凭据,不应直接贴到公开工单。

另一个实际陷阱是服务状态:本次systemctl显示生成式服务为inactive,但进程与Unix套接字实际存在。判断运行状态要结合进程、监听和宝塔/SysV服务脚本,不能因此认定PHP已经停了。

4. 第二步:备份运行时与配置,记录回退依据

本次备份放在Web目录外、仅管理员可读的目录。记录原二进制哈希、原配置选项与扩展清单;备份里可能有私有配置,不能放进知识库下载或公开Git仓库。

TASK=/root/php-security-20261022
mkdir -p "$TASK"
chmod 700 "$TASK"
tar -czf "$TASK/php82-before.tar.gz" -C /www/server/php 82
cp -a /www/server/php/56/etc "$TASK/php56-etc-before"
cp -a /etc/init.d/php-fpm-82 /etc/init.d/php-fpm-56 "$TASK/"
cp -a /www/server/panel/vhost/nginx "$TASK/nginx-vhosts-before"
/www/server/php/82/bin/php-config --configure-options \
  > "$TASK/configure-options.before.txt"
/www/server/php/82/bin/php -m > "$TASK/modules-before.txt"
sha256sum /www/server/php/82/bin/php \
  /www/server/php/82/sbin/php-fpm > "$TASK/binaries-before.sha256"

这份备份服务于PHP运行时回退,不是整机灾备。本次不变更业务数据库结构,也不把数据库导出、客户表、证书私钥等加入公开记录。如果后续还要迁移数据库、应用或主版本,应另做对应备份与恢复演练。

5. 第三步:核实无依赖后,停掉未使用的PHP 5.6

本次先查生效站点绑定、实际监听与启动方式,确认没有业务站使用PHP 5.6,再正常停止并关闭其SysV自启:

/etc/init.d/php-fpm-56 stop
chkconfig php-fpm-56 off
chkconfig --list php-fpm-56
ps -eo pid,args | grep '[p]hp-fpm: master process'
ss -xlpn | grep 'php-cgi-56.sock'

验收点是旧主进程与监听退出,启动项显示off,而不是只看停止命令返回。旧文件和配置保留,未把PHP 5.6说成“已经打上补丁”。如果你的站点仍依赖5.6,不能直接停;应先安排兼容性迁移或受维护供应商方案。官方此次提供的旧分支最小源码回移植涉及7.4、8.0、8.1,不应据此认为5.6有同等现成修复。

6. 为什么没有直接点宝塔的卸载重装,或运行yum update php

本次/www/server/php/82/bin/php不属于系统RPM包,系统包更新不能保证改到宝塔运行时。为避免升级过程中删掉现有扩展和配置,我们选择在同一8.2分支使用官方8.2.35源码,按原编译参数构建,先暂存再切换。

如果你的宝塔软件商店已经提供包含修复的对应版本,也可以先做好备份,通过面板更新;更新后仍要检查实际FPM版本、旧工作进程和业务。本文记录的是本机执行的源码更新路径,不冒充“本次亲自在面板点过”的步骤,也不保证不同宝塔版本菜单完全相同。

7. 第四步:从官方取源码,并校验SHA256

本次使用php.net发布信息里的8.2.35 tar.xz校验值,下载和校验均成功。其他版本不能套用这个哈希。

TASK=/root/php-security-20261022
cd "$TASK"
curl -fL --connect-timeout 15 --max-time 180 \
  https://www.php.net/distributions/php-8.2.35.tar.xz \
  -o php-8.2.35.tar.xz
printf '%s\n' \
  '5737b98601fe5ab1b8c5db0c82ed3158acb9283ed3081ffe2faffb9ac81455e6  php-8.2.35.tar.xz' \
  | sha256sum -c -
tar -xf php-8.2.35.tar.xz

必须得到php-8.2.35.tar.xz: OK再继续。不要为了下载成功关闭TLS证书校验,不使用来历不明的“漏洞修复一键脚本”。

8. 第五步:复用原编译选项,在隔离目录构建

本机已有编译器和原依赖,构建使用原php-config输出的选项,保留现有安装前缀;构建本身不会覆盖生产。服务器只有2核、约2GB内存,最初使用make -j1;核可用内存后仅暂停隔离目录的make并以nice10、最多2任务继续,期间观察内存和线上响应,不把本次机器的资源数字推广为PHP通用要求。

TASK=/root/php-security-20261022
cd "$TASK/php-8.2.35"
export PKG_CONFIG_PATH=/usr/local/onig/lib/pkgconfig:/usr/local/libsodium/lib/pkgconfig:
python3 - <<'PY'
import shlex, subprocess
args = shlex.split(subprocess.check_output([
    '/www/server/php/82/bin/php-config', '--configure-options'
]).decode())
subprocess.check_call(['./configure'] + args)
PY
nice -n 10 make -j2
make install INSTALL_ROOT="$TASK/stage"

INSTALL_ROOT把安装结果先放到私有暂存目录;不要删掉这一项直接安装。若configure缺依赖或make失败,应先停在这里,核错误和依赖,不覆盖生产、不无差别安装整套新环境。原参数里的PKG_CONFIG_PATH是本机依赖位置,别照搬到没有这些目录的机器。

9. 切换前必须检查什么

暂存二进制应能启动,版本正确,依赖不缺失;再用生产的CLI配置检查扩展、用生产FPM配置测试语法。此时生产仍由原二进制提供服务。

TASK=/root/php-security-20261022
STAGE="$TASK/stage/www/server/php/82"
"$STAGE/bin/php" -n -v
"$STAGE/bin/php" -c /www/server/php/82/etc/php-cli.ini -m
"$STAGE/sbin/php-fpm" -t -y /www/server/php/82/etc/php-fpm.conf
ldd "$STAGE/bin/php" | grep 'not found'
ldd "$STAGE/sbin/php-fpm" | grep 'not found'

ldd没有not found才是依赖检查通过;grep无匹配会返回1,不能机械当成构建失败。还要比较原扩展清单,重点核mysqli、pdo_mysql、mbstring、curl、openssl、gd、intl、sodium、zip等实际业务依赖。不要因为php -v成功就忽略PHP Startup或无法加载扩展的警告。

10. 生产替换、重启与本次验收结果

本次暂存二进制通过11项基础功能检查,再运行同一份公开工具的--apply路径完成切换;工具复用了本次已经按前文编译并验收的暂存产物。没有在另一台干净服务器重复全流程,也没有对各发行版、扩展组合或其他PHP分支宣称通用验证。

TASK=/root/php-security-20261022
bash "$TASK/baota-php82-security-fix.sh" --apply

脚本实际替换的是PHP82的CLI、CGI、FPM二进制、php-config/phpize等元数据工具和对应OPcache模块;不覆盖etc配置目录、不重装外部zip.so、不改站点源码、数据库结构、Nginx站点绑定或原套接字。每个文件先写同目录临时文件再原子替换。切换期间若关键步骤报错或收到退出信号,会尝试恢复私有备份并明确报告“未修复”,不会把回退当成功;本次没有故意在生产制造失败来演练回退。

随后执行:

/www/server/php/82/sbin/php-fpm -t -y /www/server/php/82/etc/php-fpm.conf
/etc/init.d/php-fpm-82 restart
/www/server/php/82/bin/php -v
/www/server/php/82/sbin/php-fpm -v

这些配置验证、重启和版本检查由工具完成,不要执行工具后又机械重启一遍。重启PHP-FPM会短暂影响动态请求,本文没有测出或承诺零中断。

本次实际验收结果如下:

同机其他站不能默认都是WordPress:我们尝试过的两个WordPress专用探测路径在其他站返回404,这只说明该路径不能用于其业务验收,不直接当升级故障;主站正确路径单独验收。另一个图片子站源站TLS出现自签证书校验失败,属于单列的证书问题,未通过关闭证书校验把它报成全站验收通过。

构建日志也并非“完全零警告”:原宝塔参数包含已废弃或不识别的选项,编译器亦输出过警告;最终以构建退出状态、实际产物、模块和运行检查为准。不能从旧参数里出现--with-xmlrpc就认定XML-RPC扩展已启用。本次修复不是一份完整的入侵调查,也没有实测所有后台写入、上传、业务计划任务或长时间高负载。

11. 回退不是恢复安全状态

若新运行时出现业务不兼容,应先暂停切换、保留错误证据,使用本机私有备份恢复必要二进制与配置并重新启动服务,再核动态业务。回到8.2.28只是业务应急回退,会重新暴露漏洞,必须配合限制暴露面并继续完成安全升级;不能把回退完成写成漏洞修复完成。

公开工具的restore_previous函数保留了自动恢复逻辑。管理员如需手动应急回退,应先核实私有备份、环境和目标;下面只恢复工具实际替换过的文件,不解压覆盖整个运行目录。仅在确认需要业务应急、已安排隔离或限制暴露面后,核实TASK与PREFIX再以root逐步执行:

TASK=/root/php-security-20261022
PREFIX=/www/server/php/82
test -s "$TASK/php82-before.tar.gz"
test -d "$TASK/replace-before"
/etc/init.d/php-fpm-82 stop
while IFS= read -r -d '' old; do
  rel="${old#"$TASK/replace-before/"}"
  cp -a "$old" "$PREFIX/$rel.rollback-new"
  mv -f "$PREFIX/$rel.rollback-new" "$PREFIX/$rel"
done < <(find "$TASK/replace-before" -type f -print0)
if test -f "$TASK/version.pl.before"; then
  cp -a "$TASK/version.pl.before" "$PREFIX/version.pl"
fi
"$PREFIX/sbin/php-fpm" -t -y "$PREFIX/etc/php-fpm.conf"
/etc/init.d/php-fpm-82 start
"$PREFIX/bin/php" -v

恢复后还须核真实进程、模块、源站动态请求与业务;步骤失败应停下定位,不能继续覆盖。这段命令解释本次真实备份布局,但没有在已修好的生产环境再次执行降级试验。直接解压覆盖整个运行目录可能覆盖新文件、权限或正在运行的进程;没有业务绑定的5.6也不应因回退82而自动重启。

12. 修复中容易误判的几件事

13. 官方依据与后续维护

PHP官方安全公告(2026-10-21)

OOM corruption官方安全通告

stale global state官方安全通告

PHP 8.2.35发布信息与源码SHA256

本次实际运行记录与通用建议分别标记;后续版本的修复状态以PHP官方及所用发行商公告为准。如果存在异常进程、未知PHP文件或账号变动,应另行开展入侵排查,升级PHP不能清除已经留下的后门。