返回 2026-05-09
📝 其他

周末在伯尼家:你的依赖项中谁在戴墨镜?Weekend at Bernie’s

nesbitt.io·2026-05-08

文章以幽默方式探讨软件依赖项的脆弱性与生命周期管理问题。通过‘谁在你的依赖链中假装自己还活着’的比喻,揭示过时或废弃库带来的潜在风险。作者暗示开发者应定期审计依赖关系,避免因‘穿西装打领带’(即表面正常)的组件导致系统崩溃。

Andrew Nesbitt

在1989年的电影里,两名初级员工来到老板的海边别墅,发现他已去世,于是整个周末都戴着墨镜推着他四处走动,以免被人察觉。其他宾客不断拍他的背、往他手里塞饮料。这之所以能蒙混过关,是因为没人仔细查看,而且每个人都强烈希望伯尼还活着。

过去几周我一直在尝试弄明白:我们依赖的众多开源包中,有多少正处于那种状态——安装时不断出现漏洞,每周吸引数百万次下载,持续接收新问题报告,却无人真正负责(就像戴着墨镜的人一样)。我现在提出这个问题,而不是几年前,是因为AI辅助的漏洞发现正在改变人们实际检查脉搏的频率。

这在某个包收到安全报告时影响最大。有时根本无人回应,保密期过后,就会发布一个CVE,却没有对应的修复版本可供参考。有时确实会有人编写修复代码,通常是报告者或临时贡献者所为,代码可能已合并到git中或停留在开放PR里,但注册表中拥有发布权限的那个账户已经消失,修补后的代码永远无法到达用户的安装命令。

Linux发行版常规处理第二种情况,因为发行版的打包者历来都会自行打补丁,无需等待上游更新;但语言包管理器则没有类似角色。注册表命名空间属于最初注册它的人,如果此人不在了,就没有人能阻止你下载未修补的tarball。虽然有针对单个应用的变通方案(上周我已详细讨论过),但这将工作量分散给每一个下游使用者,而非一次性修复发布的构件。

还有一个更隐蔽的问题:即使包本身没有漏洞,维护不善的包也会引发问题。如果它对其某个依赖项声明了严格的版本范围,而该依赖项由活跃团队维护并发布了新的主版本安全更新,解析器就无法获取修复,因为死包的约束条件不允许。所有下游项目都会被这个单行清单困在易受攻击的版本上,而清单的编辑者却不在场——这正是byroot在Bundler中使用openssl时遇到的情况。

什么是“死”包?

简单的方法是查看最后一次提交日期,把两年以上的都标为“废弃”,然后公布一个吓人的数字。我不想这么做,因为2019年后无提交的包未必就是死的。任何生态中最受欢迎的包往往只有四十行代码,完成后就无需再提交。我真正想知道的是:如果你敲门,是否有人应答?仅靠提交日志无法告诉你这一点。

因此我从ecosyste.ms的关键包集合入手,这是按下载量和依赖仓库数量综合排序的十六个包管理器中的顶级包。这给了我8,606个包,背后对应5,874个不同的仓库。对每个仓库,我提取了过去一年的提交活动、issue和PR活动、谁关闭或合并了内容、谁在注册表中拥有发布权限、最后一次发布的日期,以及针对该包的安全公告。然后将它们分为四类。

  • 活跃:在过去一年内对默认分支有定期非机器人提交,或有发布行为。
  • 休眠状态:代码几乎或完全没有更新,但过去一年中拥有写入权限的人曾关闭过问题、合并过 PR 或提交过代码,因此修复有可能被合并。
  • 已废弃:仓库已被归档,或者在过去一年中有人提了 issue 或 PR,但拥有写入权限的人未做任何回应(如关闭、合并、提交或发布)。
  • 未知状态:既无 issue 也无 PR,且无任何活动记录,因此无法测试其响应能力。
  • 判定为“已废弃”需要确凿的非响应证据,而不仅仅是缺乏活跃性——我更倾向于少算一些,也不愿将某个作者只是休了一年假的包列入名单。

    统计数据

    在 5,874 个关键仓库中,48.8% 处于活跃状态,20.2% 为休眠状态,12.1% 已废弃,18.9% 状态未知。也就是说,不到一半的项目是明确维护中的。12% 确认已废弃听起来不算多,但这 713 个仓库支撑的依赖项所关联的下游仓库总数约为 2.9 亿(这些是依赖图中的边,未去重),若再加上休眠和未知的类别,总数将远超十亿。

    按生态系统细分后,废弃比例略有波动,但在我拥有足够数据可分析的几乎所有生态系统中都达到了两位数。

    一个仓库只要发布到多个生态系统,就会在每个相关列下出现一次,因此各列总和超过 5,874。我已排除五个最小的注册中心(swiftpm、nuget、cocoapods、pub、cpan),因其样本量不足 100 个仓库,百分比基本无意义。

    Go 的 20% 已超出噪声范围,我怀疑部分原因是当仓库迁移时导入路径失效,而模块代理仍保留旧路径供安装,导致下游无人被迫察觉。npm 的废弃比例居中,但绝对数量占主导,其“未知”列尤其庞大。这主要源于 npm 关键集合的特性:成百上千的小型工具库,因功能简单,根本没人会去提 issue。

    按依赖仓库数量排序的废弃列表顶端几乎全是这类 tiny npm 工具:fast-deep-equal、fast-json-stable-stringify、utils-merge、require-directory,每个都有三到五百万个仓库依赖它,且多年无维护者活动。约三分之一的非活跃包自身零运行时依赖,属于叶子节点。最受依赖的废弃代码也是最简洁的代码——仅几十行——这令人稍感安慰(出错概率低),但也恰恰凸显了问题所在:正因为代码如此简短,也几乎没有理由让人再去读一遍。

    敲响大门

    在 713 个已废弃仓库中,有 322 个已在 GitHub 上正式归档,所有者至少贴出了说明。其余 391 个则是“伯尼们”:未归档,贡献图中常有 Dependabot 提交的绿色方块(指向无人合并的分支),issue 标签页却仍在接收输入。

    其中 321 个在过去一年内有至少一人提交过 issue。243 个在过去一年内有至少一个 PR 被打开但未合并,其中一些 PR 其实就包含了解决方案,补丁已附上,却无人能按下合并按钮。

    注册表层面的情况往往比仓库层面更糟:在 1,414 个已废弃或休眠的包中,恰好只有一个账号拥有发布权限;对许多已废弃的包而言,合理推测该账号的所有者已离职、丢失双因素认证设备,或早已忘记这个包的存在。

    我更担心的是那些看似休眠的存储桶,而不是真正死亡的存储桶,因为前者数量更多——其中156个存储桶恰好有一个拥有写入权限的人,而这个人过去一年什么也没做。这个人还收到了所有由AI生成的垃圾报告和无理取闹的临时要求,这使得独自维护变得越来越不吸引人,仿佛是在浪费夜晚时光,而每一个这样的存储库距离“死亡列”都只有一次职业转型的距离。

    为什么是现在?

    一个拥有五百万依赖项且零个CVE的死包,与其说是安全的,不如说是不被审查的;在过去十年中,这种区别几乎无关紧要。愿意花一下午时间审核2017年那个仅200行的字符串转义工具的开发者数量几乎为零,因此“没人费心去看”在实践中其实是一种非常有效的辩护理由。

    但这种理由正变得愈发站不住脚,因为同样的AI工具不仅正在填满维护者的收件箱(充斥着低质报告),而且越来越多地发现了真实问题。将模型指向一个tarball并询问其问题的成本已降至几乎为零,而为此类漏洞领取赏金或寻找立足点的人群增长速度,远远超过了能够有效处理这些报告的人群。

    在整个关键集合中,已有110个已发布的公告无法获得补丁版本——我的意思是,在公告数据中根本不存在first_patched_version,而不是说修复已在git中标记但从未发布。有些公告针对的是早在提交公告之前就已死亡的包,因此披露流程走完,CVE被发布,但没有任何可升级的版本。死列中的其他每个包,在下一次针对它的公告发布时,也都处于相同状态。

    那18.9%未知的存储库让我同样担忧,就像那12.1%已确认死亡的存储库一样,因为这些存储库在过去一年里既没有收到任何issue或PR,也没有人测试过是否还有人在线。对它们中的任何一个来说,第一条安全报告就是那个测试本身。

    很多人即将有意识地、大规模地对整个注册表运行这个测试,并且他们发现的问题中有相当一部分将是真实的。当报告落在死列中的一个包上时,目前尚无妥善的处理路径:没有像发行版打包者那样的下游维护者能以相同名称发布,而大多数注册表的废弃包政策都假设有人想接管该名称,而不是仅仅为了发布一个修复。

    将lockfile中的每一项都视为背后有维护者在处理一切事务,这一假设之所以可行,主要是因为很少收到报告。注册表及其相关社区将需要一个实际流程来处理针对无人接收包的合法CVE,而目前大多数都没有这样的流程。

    需要完整排版与评论请前往来源站点阅读。