部署 PHP 到生产站的三道安全网

9 月 28 号晚上,我在往生产站传第三个自己写的 mu-plugin。

白天加页脚备案号,晚上补 meta description,全是 PHP 文件,目标都是服务器上 wp-content/mu-plugins/ 这个目录。传第二个的时候我开始后怕:这些文件没有草稿状态,传上去之后的下一个请求就加载它——包括后台的请求。

那天晚上我把部署脚本加上了三道安全网:上传后立刻查语法,不过就删;上传前自动备份;发布前后 dry-run 和 verify 各跑一遍。当晚,第二道网自己先出了事——它把备份文件放错了地方。

为什么 mu-plugin 这么吓人

普通插件坏了,后台点「停用」还能救。mu-plugins 不吃这套:WordPress 对这个目录的规矩是里面所有 .php 文件在每个请求里无条件加载,后台没有开关,想停用只能删文件。

一个语法错的 mu-plugin 就等于前台白屏加后台白屏,管理界面进不去,只能 SSH 上服务器删文件。人在电脑前倒不怕,怕的是哪天在外面,手机上连个删文件的入口都没有。

第一道网:上传后立刻 php -l

php -l 是 PHP 自带的语法检查,只查语法不执行代码。部署脚本上传完的第一件事就是跑它:

h.upload(LOCAL, REMOTE)              # 先上传

rc, out, _ = h.run(f"php -l {REMOTE} 2>&1")
if "No syntax errors" not in out:
    err(f"语法错误,已回滚:{out[:400]}")
    h.run(f"rm -f {REMOTE}")         # 语法错直接删,别让下一个请求加载它
    return 1

注意顺序:先传上去,用服务器自己的 php 查,不过就删。窗口期只有几秒,但比在本地查完再传诚实——真正执行这段代码的就是服务器上那个 PHP 版本。

第二道网:备份,放错了目录

上传前把旧文件 cp 一份,改坏了能还原。第一版脚本就是顺手一 cp,备份和正式文件躺在同一个目录里:

cp zz-meta-tags.php zz-meta-tags.php.bak-20260928-211307

当晚跑全量校验,check_sync.py 列出 mu-plugins 的文件清单,我盯着这个 .bak-20260928-211307 看了几秒才反应过来:mu-plugins 是个没有「临时文件」概念的目录,见 .php 就加载。备份放这儿,等于在家门口埋雷。

当夜就搬了。

备份一律进 wp-content/_mu-backups/,之后的部署脚本全按这个规矩来,check_sync.py 也加了「mu-plugins 残留文件检查」这一项。

后来我查了源码

写这篇的时候,我把 mu-plugins 的加载逻辑从 WordPress 核心里翻了出来:

while ( ( $plugin = readdir( $dh ) ) !== false ) {
    if ( str_ends_with( $plugin, '.php' ) ) {    // 只认结尾 4 个字符
        $mu_plugins[] = WPMU_PLUGIN_DIR . '/' . $plugin;
    }
}

str_ends_with——只看文件名结尾是不是 .php。也就是说 .bak-20260928-211307 的结尾是 1307,WordPress 根本不会加载它。我当晚的担心,对那个具体文件名并不成立。这是核对官方源码才发现的:当时脚本注释里写的「哪怕文件名是 xxx.php.bak-20260928 也一样」,是我想当然了。哪个版本开始把判断换成 str_ends_with 的我没细究,反正我看到的当前核心就是这个写法。

但搬出去这个决定我不改。这个函数没有任何别的容错,哪天备份命名成 backup.php,或者手动 cp 一份忘了改后缀,两份定义同名函数的插件就同时跑起来。mu-plugins 只认结尾四个字符,不给你第二次机会。

WordPress 判定 mu-plugins 加载的规则:str_ends_with 只认结尾 .php,.bak-时间戳 走的是不加载分支,真正的雷是任何以 .php 结尾的文件
WordPress 判定 mu-plugins 加载的规则:str_ends_with 只认结尾 .php,.bak-时间戳 走的是不加载分支,真正的雷是任何以 .php 结尾的文件

顺带说一句,这个目录之前还闹过另一回事:两个正式插件一个往页面输出 index、一个输出 noindex,百度同时收到两个相反信号。那是另一个坑,展开写过:一页两个 robots 标签,百度该听谁的。

第三道网:发布前后各验一遍

apply 之前先 --dry-run,只打印计划不动文件;apply 之后 --verify,拿 HTTP 状态码和页面内容说话。部署脚本的「成功」从来不信它自己报的——推送按钮显示成功、实际一个请求都没发的那次教训在这:手动推送按钮,撞上 6 小时去重。

mu-plugin 部署流水线:备份进 _mu-backups、上传、php -l 失败即删、HTTP 校验失败即回滚,每一步都有失败出口
mu-plugin 部署流水线:备份进 _mu-backups、上传、php -l 失败即删、HTTP 校验失败即回滚,每一步都有失败出口

现在每次全量校验,mu-plugins 的文件清单都会被列一遍,看到不该在的 .php 就报警。那晚先看见 .bak-20260928-211307 的,就是这道检查。

安全网也会出错,所以安全网自己也得有人查。

发表评论