我维护着一个叫 capdrift 的工具,它能读取一个 Dart 包并报告这个包具备哪些能力。最近我做了一件有点“笨”的事:把 pub.dev 上下载量最高的 162 个包全部跑了一遍,保留原始输出,然后认真看了里面到底有什么。

方法很简单:对每个包的最新版本执行 capdrift inspect,基于解析后的 AST(抽象语法树)分析,不执行任何代码。162 个包,共产生 1,805 条发现,零分析失败。只有 4 个包返回了不完整的分析结果,名字列在文末。

打开网易新闻 查看精彩图片

结果让我有点意外:近六成的发现,位于永远不会触达依赖者的代码。一个打开 socket 的测试,它只是测试;一个读取文件系统的示例应用,它只是示例。当你把包加进 pubspec.yaml 时,这两者都不会运行。

这不是四舍五入的误差。按单个包来看,情况相当扎眼。

dio 是生态中第二流行的 HTTP 客户端。一个扫描器如果遍历压缩包里的每一个 .dart 文件,会报告 142 条发现。其中只有 4 条是 dio 的使用者真正会接触到的。其余 138 条,来自它自己的测试套件和示例应用。

如果你见过一个团队关掉安全扫描器,这就是背后的机制。不是工具错了——那 142 条发现里每一条都是真实文件里的真实 API 调用。问题在于,工具没有说明哪些才可能真正要紧。一份 97% 内容都无关紧要的报告,会训练人们直接关掉标签页。

只看消费者会运行的代码,情况完全不同

如果只统计消费者实际会运行的代码,结果是 81 个包中共有 633 条发现

  • 47 个包(29%)完全没有能力,它们只做计算和返回
  • collection、meta 以及大部分纯 Dart 工具层都属于这一类
  • 这比我预想中要健康得多,值得说出来——因为“供应链”类文章往往暗示一切都在着火

按数量算,生态中一半的包是惰性的。风险集中在比原始发现数所暗示的要小得多的集合里。

我最关心的是运行时加载代码的类别,因为一个在运行时加载代码的包,能做静态分析永远看不到的事:

collection 是让我意外的一个。它是 Dart 团队维护的包,作为依赖来说几乎无聊到极致。它的 mirrors 用法仅限于自己的测试支持代码——这正是这篇文章想说的表面区分——而只看发现数量的话,我根本不会知道这一点。

构建钩子:一个真实但暂时空白的攻击面

我原本打算从构建钩子开始写。hook/ 目录里的代码会在 pub get 时、在你运行自己应用的第一行代码之前,就在你的机器上执行。这是 Dart 生态中最缺乏检视的表面。

162 个包里,零个带有 hook/ 目录。不是一个两个,是完全没有。

在相信这个结果之前,我确认了这不是我自己的 bug:capdrift 的测试语料包含三个合成钩子夹具,其中一个会在构建钩子里 shell 出去调用 curl,在该语料上的召回率是 11/11。检测器确实会触发。只是目前确实没什么可找的——因为 native-assets 构建钩子还很新,已有的成熟包都早于它们出现。

这是一个真实的发现,我宁愿发布它而不是悄悄删掉。攻击面是真实存在的,只是现在还没有猎物。