本文翻译自《What NPM Should Do Today To Stop A New Colors Attack Tomorrow》。
2022/01/10
周末,一位名叫Marak Squires的开发者故意破坏了他流行的NPM包colors和不太流行的包faker。在我写这篇文章的时候,NPM声称有18971个包直接依赖colors和2751个包直接依赖faker。据Open Source Insights统计,间接依赖colors的包至少有42000个。许多流行的NPM包都依赖于这些包。
NPM设计中的一个错误之处意味着,一旦最新版本的colors发布,新安装的基于colors的命令行工具就会立即开始使用它,而没有测试它是否与每个工具兼容。(剧透提醒:事实并非如此!)
具体的错误之处在于,当你使用NPM安装软件包(包括命令行工具)时,NPM会根据package.json文件中列出的需求以及当时的世界状态来选择依赖项版本,并且优先安装每个依赖项的最新版本。这意味着,从Marak更新colors包的那一刻起,aws-cdk和其他工具的安装就被中断,错误报告开始滚滚而来,就像下面这个那样:
相关错误也出现在apostrophe,cdk8s,compodoc,foreversd,hexo,highcharts,jest,netlify,oclif等包里。
今天,NPM用户可能会对Marak感到沮丧,但Marak搞的破坏只是在终端上输出一些垃圾信息。情况其实可以更糟。即使忽略了这种故意破坏,非故意的错误也总是发生。从本质上讲,每一个开源软件许可证都指出,代码是在没有任何保证的情况下提供的。现代软件包管理器的设计需要预见并减轻这种风险。
任何运行现代生产系统的人都知道测试,然后是逐步或分阶段的部署,即在很长一段时间内逐步部署对运行中的系统的更改,以减少意外地同时删除所有内容的可能性。例如,上次我需要更改谷歌的核心DNS区域文件时,对该更改进行了多次回归测试,然后在24小时内一次一个地部署到谷歌的四个名称服务器中的每一个。回归测试检查了不该出现的意外情况,然后给了自动化系统和可靠性工程师足够的时间来逐步部署。
NPM的设计选择恰恰相反。最新版本的colors在所有人有机会测试它之前就被推广到了所有依赖它的包中,而且没有任何逐步升级的方法。用户现在可以通过固定其所有依赖项的确切版本来禁用这种行为。例如,这里是对aws-cdk的修复。这不是一个好答案,但至少这是能用的。
对于NPM和类似的包管理器来说,正确的前进道路是在安装新包时不要再优先安装所有依赖项的最新版本。相反,它们应该优先安装实际测试过的依赖项,或者尽可能接近这些版本的版本。我称之为高保真构建。相比之下,在周末安装aws-cdk和其他包的人得到了低保真度的构建:NPM插入了开发者从未测试过的新版本的colors包。用户在周末测试自己的项目时,测试失败了。
高保真度构建解决了测试问题和如何逐步升级问题。在aws-cdk作者有机会测试并在新版本的aws-cdk中引入之前,新版本的colors不会被aws-cdk包所接受。在那之后,所有新的aws-cdk包都会引入新的colors包,但所有其他包仍然不会受到影响,直到它们也测试并正式引入了新版本的colors包。
有许多方法可以生成高保真度构建。在Go中,一个包声明每个依赖项所需的最低版本,这就是构建项目时所使用的依赖项的版本,除非同一个项目里有某个依赖项依赖其中某个依赖项的更新的版本。然后,它只使用这个特定的更新版本,而不是刚刚在周末发布的、完全未经任何人测试的版本。有关这种方法的更多信息,请参阅“Go中的版本控制原则”。
NPM还有一个npm shrinkwrap命令和一个npm ci命令,这两个命令似乎都可以在某些有限的情况下解决这个问题。大多数受colors包影响的命令的作者和用户今天应该仔细研究这些命令。感谢NPM提供了这些命令,但他们不应该总是依赖这些命令。下一步应该是NPM安排这种保护默认发生。然后,当为依赖项安装新的包时,需要同样的保护,而不仅仅是在安装命令时。所有这些都需要更多的工作。
其他语言的包管理者也应该注意到这个问题。大多数包管理者在没有任何类型的测试的情况下自动采用新依赖项,也没有提供逐步升级软件包的方法,Marak的行为强调了这个问题,这给我们所有人带来了巨大的帮助。早就该解决这些问题了,否则下一次只会更糟。