最近服务器上的 PHP 7.0.14 让我有点不放心。

原因很简单:PHP2Shell 漏洞出来以后,继续让一个多年没有维护的 PHP 版本承担线上请求,多少有点心存侥幸。PHP 官方公告中提到的两个漏洞是 CVE-2026-109618 和 CVE-2026-109619,前者与 OOM 处理有关,后者与旧的全局状态处理有关。攻击者不需要登录,在特定请求条件下就可能触发内存破坏,进一步以 PHP-FPM 进程的权限执行代码。

官方已经把 PHP 8.2.35 列为修复版本,相关资料可以看这里:

这件事情本来只是一次 PHP 安全升级,做到后来,却变成了 Typecho 1.1 升级 Typecho 1.3。中间遇到的几个问题,也记录一下。

先看看旧版本能不能直接换

旧站使用的是 Typecho 1.1。我先拿 PHP 8.2 对旧代码做了一遍语法检查,结果很快就发现了问题:var/Json.php 使用了 PHP 8 已经不再支持的花括号下标写法。

$string{$index}

这不是一个普通警告,而是 PHP 8 直接拒绝执行的致命错误。继续在 Typecho 1.1 上打补丁当然也能运行,但以后每次升级 PHP 都要重新翻旧代码,维护起来并不划算。

于是,Typecho 也一起升级到 1.3。Typecho 1.3 的核心代码已经清理了一批旧实现,自动加载、命名空间和类型声明也更接近现在的 PHP。更重要的是,后续再升级 PHP 时,至少不用从 Typecho 1.1 的旧核心开始排查。

不过,核心升级并不等于主题和插件也自动升级。配置文件、自定义插件、主题修改和上传文件,都需要单独保留和验证。

先复制一份站点

我没有直接动线上目录,而是先复制出一个测试目录:

原站:/srv/www/example-typecho
副本:/srv/www/example-typecho-1.3

复制以后,用 Typecho 1.3 的官方代码覆盖副本,但保留 config.inc.php、自定义插件、主题修改、上传目录和站点附件。数据库最好也复制一份,至少第一次测试时不要碰线上数据。

后台新增文章也要单独验证

前台页面能打开,不代表后台已经完全兼容。升级以后,我先在后台打开新增文章页面,遇到过一次 500。这个问题最后定位到站点配置文件中的临时调试内容,清理配置并重新加载 PHP-FPM 后,编辑页面恢复正常。

真正提交文章时又遇到一个更容易误判的问题:后台请求返回 500,但文章其实已经写进数据库了。如果看到错误页面就马上重复提交,很容易生成重复文章。我的处理顺序是先查文章列表和数据库状态,确认文章已经是已发布,再检查固定链接、分类和标签是否写入。

这次提交后,标题和正文已经保存,但固定链接、分类和部分标签没有完整保存。我没有再次点击发布,而是补齐文章的 slug、分类关系和标签关系,再用公开地址检查结果。后台返回 500 时,应该同时检查 PHP-FPM 日志、数据库记录和公开页面,不能只根据后台响应判断是否成功。

后台新增文章的检查清单可以简单记成几项:新增页能否打开;Markdown 开关是否启用;提交后文章是否只生成一条;固定链接是否正确;分类和标签是否存在;公开页面是否能看到完整正文。

PHP 8.2 单独跑起来

为了不影响旧站,PHP 8.2 使用单独的安装目录和 FPM 端口:

/path/to/php8
127.0.0.1:9282

FPM 池大致如下:

[php82]
listen = 127.0.0.1:9282
user = nobody
group = nobody
pm = static
pm.max_children = 4

然后给副本配置一个临时域名,用 Caddy 把请求送到 9282 端口。这样测试失败时,旧站还可以继续使用旧 PHP。

扩展不能少

PHP 主程序启动了,页面不一定能打开。Typecho 1.3 处理中文内容时会调用 mb_strlen(),如果没有 mbstring,首页可能看起来正常,某篇文章却直接返回 500。

/path/to/php8/bin/php -m | grep -E 'ctype|mbstring|PDO|pdo_mysql'
/path/to/php8/sbin/php-fpm -i | grep 'Loaded Configuration'

这次遇到的第一个真实错误就是:

Call to undefined function Typecho\mb_strlen()

补上 mbstring,并重载实际提供网站请求的 PHP-FPM,页面才真正恢复。

正式切换之前,先看几个页面

我至少检查了首页、关于页面、带中文和代码块的文章、RSS、分类页、后台登录、评论、上传、自定义插件和主题。检查过程中,首页和普通页面都正常,唯独部分文章的内容显示不完整。HTTP 状态是 200,PHP 日志也没有新的致命错误,问题看起来不像 PHP-FPM。

HTML 为什么只显示一部分

把页面原始 HTML 抓下来以后,问题就比较明显了。EditorMD 插件把同一篇 Markdown 处理了两次,结果变成了这样:

<div id="md_content_2" class="md_content">
    <textarea>
        <div id="md_content_1" class="md_content">
            <textarea>文章内容</textarea>
        </div>
    </textarea>
</div>

外层 textarea 里面又放了一个完整的 HTML 容器。浏览器不会把它当成正常的文章结构,前端脚本随后读到的内容也是空的,所以页面看起来像是文章被截断了。

这个问题不是 PHP 8 的语法错误,而是 Typecho 1.3 调用主题和插件钩子的方式变化后,把旧插件里重复包装的问题暴露出来了。

前端只处理原始容器

EditorMD 原来的代码会遍历所有 .md_content。第一次渲染之后,预览容器仍然带着这个类,于是又被处理一次。改成只选择还没有成为预览容器的元素:

var markdowns = document.querySelectorAll('.md_content:not(.editormd-html-preview)');
$(markdowns).each(function () {
    var markdown = $(this).children('textarea#append-test').text();
    editormd.markdownToHTML($(this).attr('id'), {
        markdown: markdown,
        htmlDecode: true
    });
});

服务端不要重复套一层

首页摘要还会触发一次内容处理,所以服务端也加一层保护。如果传进来的内容已经是 md_content,就直接返回。修复以后,首页每篇文章只生成一个 md_content,文章页也不再出现 .md_content .md_content 嵌套。原始 HTML 和浏览器 DOM 都要检查,不能只看返回码。

最后再切换正式站点

测试确认以后,把正式站点的 Caddy 根目录和 FastCGI 端口切到新副本:

example.com {
    root /srv/www/example-typecho-1.3/
    fastcgi / 127.0.0.1:9282 php
}

修改后先检查配置:

caddy -conf /etc/caddy/Caddyfile -validate

确认没有问题,再重载 Caddy,最后停止 PHP 7.0-FPM。旧目录和旧配置先保留一段时间,真有问题时,只要把根目录和端口切回去就能回滚。

总结

这次升级的顺序大致是:因为 PHP2Shell 风险停止使用 PHP 7.0.14;单独安装 PHP 8.2.35 和 PHP-FPM;复制 Typecho 站点并保留定制;将 Typecho 1.1 升级到 1.3;补齐 mbstring 等扩展;检查文章、首页和最终 HTML;修复 EditorMD 的重复包装;最后切换 Caddy 和正式 PHP-FPM。

PHP 升级真正麻烦的地方,往往不是把新版本编译出来,而是确认每个请求、每个插件和每段 HTML 都已经走到了新的运行环境里。

标签: php, Typecho, PHP2Shell, PHP8, PHP-FPM, 安全升级, 兼容性

添加新评论