独立开发·阅读约 1 分钟·
一个 174 行的临时 PHP 补丁被装了近 2000 万次,作者为什么选择让它退休

一个 174 行的临时 PHP 补丁被装了近 2000 万次,作者为什么选择让它退休

2014 年为 AOL 内部系统写的临时 polyfill,被 Packagist 安装了近 2000 万次,至今每月还有 40 万次下载,还顺着 WPML、Debian、Ubuntu 流进了无数网站。作者最终选择弃用而不是找人接手——理由和供应链安全有关。

原文来源:Jake A. Smith — 一个为了赶 PHP 升级临时写出的 174 行 polyfill,12 年里被安装了近 2000 万次;作者在修复、交接和弃用之间,选择了最不讨好但风险最低的那条路。

十二年前,Jake A. Smith 写了 174 行 PHP,用来给 AOL 的内容管理系统打个补丁。写完顺手发到了 Packagist,想着「万一别人也需要呢」。结果这个包至今被安装了近 2000 万次。前几天,他把它标记为弃用。

一个临时的补丁

2014 年,团队正在把 AOL 的 CMS 从 PHP 5.2 升级到 5.3。这次升级的一部分工作是去掉 pecl_http 扩展的 1.x 版本,而那个扩展里有一个叫 http_build_url() 的函数。CMS 要处理大量 URL,代码里几十处都在调它——他并不打算动那些调用。

这个函数看上去不算复杂,与其改一堆调用点,不如自己实现一个:只在真正的函数不存在时才定义自己的版本,老代码完全感知不到变化。

那时 Composer 刚火起来,分享这个包很容易。他当时的预期是能用上一两年,等 PHP 社区找到更好的方案就被淘汰。

—— 广告 ——

结果一点都不临时

它没有被淘汰。从 Packagist 安装的次数接近 2000 万,至今每月仍有 40 万次以上的下载。

而且 Composer 只是整个传播链条的一部分。WordPress 上装机量领先的多语言插件 WPML 直接把这个 polyfill 打包进了自己的代码库,而 WPML 官方称自己装在 150 多万个站点上。域名处理库 idna-convert 也依赖它——它就这样进入了法国内容管理系统 SPIP 的源码,又被打包进 Debian 和 Ubuntu。

把这些加起来,你访问过的某个网站很可能还在跑他当年写的代码。这是他从没想过的。

回头再看这个包

真正意识到它扩散得多广,是几个月前的事——那是他多年来第一次翻开这个包。

他当然知道有人在用。到 2021 年,他已经离开 PHP 好些年了,下载量的数字让他觉得意外,于是公开征集新的维护者。当时有三个人报名。但就在发出征集后不久,家里意外失去了一位亲人,整个世界被翻了个底朝天,他没有跟进,这件事「责任在我」。等生活重新稳定下来,别的目标占了上风,这个包就被忘了好几年。

除了下载量,他还看到了几个 GitHub issue。其中一个说的是:把一个路径拼接到以斜杠结尾的 URL 上时,路径里所有的字母「a」都会被吃掉。

说它简单?它并不简单。在一行写着 // Workaround for trailing slashes 的注释下面,他的代码会给路径补上一个「a」,这样永远有最后一段可以切掉,然后靠查找替换把它切掉。当路径本身以斜杠结尾时,最后一段正好就是那个「a」,于是这次查找替换把路径里其他所有「a」也一起带走了。

他很惊讶这个 bug 能藏这么久没被发现。

于是摆在面前的选择有三个:快十年没碰 PHP 了,重新捡起来修;把包交给当年报名的三个人之一;或者继续让它躺在那儿。

三个都不选

他的答案是:这个包从一开始就该是临时的,那就让它退休。

PHP League 的 URI 库多年来一直是社区的标准答案,而 PHP 8.5 已经把符合标准的 URI API 内置到语言本身(这一点是 jawira 提醒他的)。两者都比 2014 年那个 174 行的补丁更好。继续维护这个包,只会推迟大家本来该做的那次迁移;而交接给别人,会在推迟之上再叠一层风险。

他并不怀疑那些愿意接手的人,ozh 也一直在为 YOURLS 维护一个 fork。但一个装机量巨大的包换上一个下游没人验证过的新维护者,恰恰是攻击者最想看到的局面——xz Utils 后门事件就是最好的例子(他推荐了 Veritasium 关于这件事的视频)。

所以这个包还能继续被安装,但不会再有新的修复,包括那个吃掉字母「a」的 bug。理由也很实在:这么多年没人动过它,哪怕只改一行,都可能对某个使用者产生预料之外的后果,而且没人在旁边支持。

README 里写了切换方案。

他在文末感谢了所有发过 PR、提出愿意接手、以及在他早就停止阅读 issue 之后还坚持提单的人。

还有一个附注:AOL 的 CMS 从来没有从那个「临时」补丁上迁移走。它一直跑在那上面,直到整个平台在 2020 年前后关停。

一个临时决定如何变成别人的基础设施

这件事里最值得独立开发者琢磨的,不是那个「吃掉字母 a」的 bug 有多滑稽,而是责任的传播方式。

作者写这 174 行的动机非常单纯——让自己和同事少改几十处调用点。Composer 让分享变成一行命令,于是同样的代码被 WPML 打包、被 idna-convert 依赖、进而进入 SPIP、进入 Debian 和 Ubuntu 的软件包。每一层都是「顺手复用」,每一层都在放大同一个人的临时决定。到最后,一个连作者自己都快十年没看过的文件,成了几百万人间接依赖的基础设施。

而对写下它的人来说,退出比接手更难处理。想维护,意味着要重新捡起离开十年的语言和生态;想交接,就得为下游几百万人评估一个陌生人的可靠程度——这恰恰是 xz Utils 事件留下的教训:攻击者不需要攻破原作者,只需要等到有人愿意接手。

他选了第三条路:不修,不交,公开说明。代价是那个已知的 bug 就留在那里,换来的是不会有新的风险被引进来。对一个个人项目来说,这是个很现实的选择——维护者的精力是有限的资源,而老实说清楚「我不再维护了」,比勉力维持一个「看起来有人管」的假象要负责得多。

分享到
微博Twitter

© 2026 四月

原文链接:https://www.aprilzz.com/indie/temporary-php-fix-20m-installs