3564 字
18 分钟
互联网至暗时刻,本来只想更新 PHP,结果把两台服务器翻了个底朝天

我原本以为这次维护的剧情很简单:看到 PHP 安全公告,升个级,确认网站还能打开,收工睡觉。

结果顺着 PHP 往下摸,后面站着 Node.js,Node.js 后面套着容器,容器底下又牵出 Go 的编译依赖。等我一路摸到 Docker 引擎的时候,旁边那块只有 30GB 的小硬盘开始报警了:再塞新镜像,它就当场去世给你看。

就这样,一个“应该十来分钟搞定”的小更新,硬生生变成了横跨 10 月 21 日、22 日两天的服务器大扫除。趁着记忆还热乎,把这两天踩的坑、升的级、腾出的 3.9GiB 空间,还有没擦完的屁股记一下。

1. 看着漏洞报告,差点犯了“无脑升级”的病#

事情的起因是一份 Web 后端漏洞检索报告,里面密密麻麻列了一堆运行时、框架、反代和数据库的安全问题。

盯着报告看久了,人很容易上头,恨不得把里面提到的所有软件挨个 apt upgrade 和 docker pull 一遍。但冷静下来想想,瞎动手之前得先搞清楚:我这些服务到底是怎么跑的?

手头这两台服务器分工其实很明确:

  • 博客服务器:跑着纯静态博客,外加一个用 PHP 写的随机背景图 API。真正要操心的是静态产物、PHP-FPM、Web 入口和同机跑着的其他服务。
  • 机器人服务器:在 Ubuntu 上裸装了 1Panel,用它管着 AstrBot、LLBot、PMHQ、OpenResty 和代理等一堆容器。这一摊子就得把宿主机程序、容器镜像和里面的运行时拆开看。

另外像 Umami、PostgreSQL、NGINX 和 OpenSSL 也在待办名单上。这些得按服务单独盯着,不能因为外层升了级,就顺手给里面全打上勾。毕竟漏洞报告只是个引子,真正在机器上跑的版本、配置和请求路径,才决定了这台服务器到底慌不慌。

2. PHP 升完了,但跑着的是新版吗?#

先拿万恶之源 PHP 开刀。机器上原本是 8.5.2,这次升到了 8.5.12(另一处 PHP 环境也顺带确认了补丁版本)。

PHP 8.5.12 官方更新记录里列了这次重点关注的两个安全修复。当然,漏洞能不能被利用还得看具体的运行方式和配置,倒也不用自己吓自己,一看到旧版本号就觉得马上要被匿名 RCE 了。

真正容易踩坑的是验证环节。很多人升完级,在终端里敲个 php -v 看到数字变了就觉得完事大吉。但命令行调的是 PHP CLI,网站请求走的是 PHP-FPM,它俩完全可能指着不同的路径,甚至老进程根本没重启。

所以我特地去核对了实际跑着的 PHP-FPM 进程,又测试了随机背景图 API 和全站 HTTPS 访问。确认全链路都在用新版本干活,PHP 这一项才算真正划掉。

擦了把汗,转头看了一眼旁边的服务——好戏才刚开始。

3. 静态博客,真就高枕无忧?#

我的博客是用 Astro + Svelte 搓的,构建出纯静态文件后扔给 NGINX。线上压根没有常驻的 Node.js 后端。

这种架构的好处是心安:就算 Node.js 10 月 21 日安全公告里爆出了 Brotli、DNS 和 TLS 的问题,它也威胁不到我根本不存在的“线上 Node 后端”。不过为了构建环境干净,我还是把 CI 和发布流程里的 Node.js 基线从 24.22.0 统一升到了 24.22.1,省得各个配置文件里版本打架。

既然是静态站,真正会跟访客浏览器打交道的只有生成的 HTML 和脚本。以前总觉得“反正文章都是我自己写的,能有什么问题”,这次仔细审了一遍代码,还真拎出两个隐患:

一个是文章页的 JSON-LD 结构化数据。标题、摘要这些元数据序列化之后会直接塞进 HTML 的 <script> 标签里。万一哪天内容里出现了 </script>,光靠标准的 JSON 序列化可拦不住它提前闭合标签。这次专门换成了适配脚本标签的安全序列化方案,老老实实把它锁在数据层里。

另一个是 GitHub 仓库卡片组件。它的参数会直接参与链接和脚本拼装,这次也补上了严格的仓库名格式校验和安全序列化。

说实话,这两处都得有人能篡改我的文章元数据或者组件参数才能触发,排查下来没发现游客能直接控制输入的入口。但修好它们,主要是防着以后自己在模板或内容上翻车。

顺手还干了件事:把部署用的 GitHub Action 锁死到了具体的 Commit SHA,防止上游标签被篡改;同时把安全回归测试塞进了构建流水线。这些改动全都在 PR #19 里合入了。发布时的体检单还算漂亮:

  • 类型检查、代码质量、32 个页面构建、27 页搜索索引生成全部通过;
  • 新增的 5 项安全测试和 6 项构建产物回归测试 全绿;
  • 锁文件依赖审计 0 漏洞告警(当然,这只代表 npm 锁文件里已公开的记录干净,管不到服务器上的 NGINX 和数据库);
  • 自建服、Cloudflare 和 Vercel 三端自动部署一次过。

博客这边利索了,接下来轮到容器给我上课。

4. 镜像是新的,不代表里面的零件也是新的#

这次排查最让我后背发凉的发现,是 应用版本升到了最新,但官方镜像里塞的运行时还是老的。

最典型的就是 LLBot。我把它从 8.3.0 升到了官方最新的 8.3.2,本来以为完事了,结果进容器里一查,Node 版本还停留在 24.21.0。

合着我只升了个寂寞?

没办法,只能自己动手丰衣足食,基于官方版重新打了一个内置 Node 24.22.1 的本地修补镜像。旁边的 AstrBot 也是同病相怜:应用本身保留在 4.29.1,但我进去把它的 Node 从 24.14.1 拽到了 24.22.1,顺手还把里面的 Starlette 框架升到了 1.0.1(Starlette 官方公告提到异常 Host 可能导致路由路径判断不一致,虽然得看应用具体怎么写鉴权,但升了总比留着强)。

除了 Node,Go 编译的二进制文件也是个重灾区。Go 标准库出了安全更新,光在宿主机上装个新版 Go 是没用的,已经编译好的程序不会自动变异。

所以我把这套逻辑彻底理了一遍,两天里在这台机器上动过的刀子都在这儿了:

组件升级前状态这次怎么处理的
LLBot应用 8.3.0升到官方 8.3.2,并自制镜像把内部 Node 补到 24.22.1
AstrBot应用 4.29.1 (Node 24.14.1)保持 4.29.1,内部 Node 升至 24.22.1,Starlette 升至 1.0.1
PMHQ9.0.3直接更到 9.0.6
sing-box 服务端应用 1.14.3保持 1.14.3 不变,拉源码用 Go 1.26.10 重新编译(客户端没动)
1Panel2.1.8升到 2.4.1,核对确认官方二进制已用上 Go 1.27.3
Docker Engine29.4.0升到 29.9.2,底层为 Go 1.26.10
containerd原默认版本同步升到 2.4.2,底层为 Go 1.27.3

像 Go 这批修复(参考 Go 发布历史 和 1Panel 2.4.1 发行说明),核心就是去查正在跑的二进制到底是用哪个版本的 Go 编译出来的。

这波操作下来,算是把“看表面版本号”的习惯彻底改成了“掀开盖子看底盘”。不过自己搓本地镜像也留了个包袱:以后要是直接切回上游官方镜像,得记得先进去看一眼他们把 Node 升上去没有,免得一拉新镜像,又把老漏洞请了回来。

5. 别光看绿灯,拉出来溜溜#

升完这一大串组件,要是截个版本号的图就去发朋友圈宣布胜利,多少有点自欺欺人。另外得老实交待:这次维护是有短暂停机的——提前做好了备份,但升 1Panel 时重启了面板,升 Docker 时容器也全都跟着歇了一会儿,算不上什么“丝滑零停机”。

等服务全拉起来后,我挨个跑了一遍真实测试:

  • 网站与反代:HTTPS 访问正常,背景图 API 吐图正常(当然,这只说明链路通了,反代配置的深度审计还得另算)。
  • AstrBot 与 Python 依赖:用正常和故意构造的异常 Host 去敲受保护接口,全都老老实实返回未授权;Python 依赖兼容性检查和回归测试也都过了。
  • 代理链路:重编译后的 sing-box 服务端,实测 HTTPS 代理请求正常。
  • Docker 与机器人容器:容器清单核对无误,LLBot 和 PMHQ 的健康检查都是绿的。

但这里有个特别容易踩的盲区:容器健康检查通过,不代表机器人真的能收发 QQ 消息了。因为重启折腾之后,QQ 还得重新扫码登录,真正的端到端消息收发验证还挂在待办上。

6. 漏洞还没修完,30GB 硬盘先喊救命了#

机器人服务器就挂了一块 30GB 的硬盘。

前面又是拉新镜像、又是自己编译、又是构建本地修补镜像,缓存和临时文件疯长。折腾到一半我敲了下 df -h,差点一口气没上来:可用空间只剩 2.7GiB 了。

剧本瞬间从“网络安全保卫战”变成了“抢救硬盘大作战”。再不腾地方,下一次更新连镜像都解压不开。

清理分了两步走。第一步先把这次折腾出来的临时文件和构建缓存全扬了;第二步去排摸历史旧镜像。注意,不能看哪个容器没在跑就闭着眼删镜像,有些停止状态的容器身上还挂着引用呢。

仔细核对完所有容器(包括 5 个运行中和 4 个停止的容器)的引用关系后,我精准送走了三个彻底没人要的远古镜像:

  • linyuchen/llbot:7.11.4
  • linyuchen/llbot:7.12.2
  • linyuchen/pmhq:7.3.2

回滚备份、业务数据卷和系统日志全都完好保留。两轮扫除下来,一共腾出了大概 3.9GiB 的空间,可用空间回血到 6.6GiB,磁盘使用率降回 77%。虽然依然不算富裕,但起码不用一边敲命令一边提心吊胆了。

7. 还没打完的仗:留下的待办清单#

写到这儿好像挺圆满,但翻开记事本,其实还有一屁股债没还完。安全维护最忌讳的就是“前排修了七八个,后排漏掉两三个,对外宣称全搞定”。

截至 10 月 22 日,这几个坑还排着队等我填:

  1. Umami (3.0.3) —— 头号优先级:官方安全公告爆出了令牌混淆导致的授权缺陷,存在被匿名读取统计数据的风险,这个必须尽快升。另外它容器里的 Node.js、Next.js 和 sharp 版本也挺老了,虽然还没证实有直接的 RCE 路径,但也得一并处理。
  2. PostgreSQL (15.17):得升到 15.19。这次重点核对的两个代码执行漏洞(CVE-2026-14669 和 CVE-2026-16239)前提都是攻击者已经拿到了数据库权限,暂时没那么火烧眉毛,但其它修复还是得做好备份后抓紧升上去。
  3. NGINX (1.26.3) 与 OpenSSL:我开了 HTTP/3,还得结合发行版补丁、构建及运行时的 OpenSSL 版本做一次彻底更新。顺带一提,之前看报告时差点自己吓自己——NGINX 官方安全列表里那个 HTTP/3 释放后使用漏洞(CVE-2026-42530)影响的是 1.31.0–1.31.1,我的 1.26.3 并不在这个坑里。纠正归纠正,常规更新还是不能省。
  4. OpenResty 与管理入口收紧:OpenResty 等组件还没轮到更新;另外最小权限部署、管理后台访问限制和端口隔离还得继续往紧里拧。

8. 日志里妖魔鬼怪很多,但先别自己吓自己#

这几天翻日志的时候,不出意外看到了满屏的互联网底噪扫描,还有不少专门冲着 Server Actions 来的畸形请求。

刚看完漏洞报告的人,看到这种日志最容易脑补出一整部黑客帝国。但客观来说,“攻击请求打到了门口”和“门已经被撬开了”完全是两码事。

在现有的日志覆盖面和文件基线范围内,我仔仔细细查了一圈,没有发现任何被成功入侵的实锤证据。当然,受限于历史日志的完整度,我也没法拍着胸脯开一张“本服务器自盘古开天辟地以来绝对纯洁”的证明。保持警惕,按部就班把剩下的洞补上,比盯着扫描日志焦虑管用得多。

9. 写在最后#

回头看这两天,最大的收获其实不是把表格里的数字改大了几号。

以前更新软件,看到终端提示 Success 就拍拍屁股走人了。这一趟折腾下来,算是被逼出了一套强迫症流水线:升完 PHP 去查实际接活的 FPM;改完静态站去把构建环境、输出产物和服务器入口拆开审;升完容器钻进镜像里看 Node 版本;看到 Go 漏洞去查二进制的编译工具链;甚至连那块只有 30GB 的小硬盘,都在用实际行动教我怎么留好回滚余地。

我本来真的只想花十分钟更新一下 PHP。

结果最后给两台服务器做了场全麻大手术,还顺手从床底扫出了三个早该退休的老镜像。歇口气,明天还得继续去会会 Umami 和数据库。

互联网至暗时刻,本来只想更新 PHP,结果把两台服务器翻了个底朝天
https://bbcslook.top/posts/guide/security-maintenance-2026-10/
作者
BBCSLOOK
发布于
2026-10-22
许可协议
CC BY-NC-SA 4.0

评论区

CC