依赖应直接从 VCS 获取
摘要
作者认为,直接从 VCS(如 Go 语言的做法)获取依赖比使用带有发布步骤的包注册中心(如 Ruby、npm、PyPI)更安全且更易于审计。
暂无内容
查看缓存全文
缓存时间: 2026/07/05 22:34
# 依赖项应直接从 VCS 获取
来源:https://www.arp242.net/deps-vcs.html
过去一个月,我在新工作中使用 Ruby 写代码。在过去十年大部分时间写 Go 之后,这算是一次有趣的场景切换。我以前(多年前)也写过 Ruby,我无法明确告诉你哪个“更好”——Ruby 几乎在所有方面都与 Go 截然不同,但我发现两者各自都能有效地完成任务。
我觉得 Go 明显更好的一个方面是依赖管理,尤其是安全方面。Go 并非对恶意依赖免疫,但它更具抵抗力,主要原因是它没有“发布包”这一步。
---
在 Go 中,依赖项通过 URL 标识,例如 `github.com/user/pkg`。Go 会识别所用的 VCS(此处为 git),并拉取你在 `go.mod` 文件中指定的标签或提交。`go.mod` 文件既充当依赖规范,也充当“锁定文件”。它列出确切的版本,没有 `~> 1.1` 这样的写法。它同时包含直接和间接依赖,并列出完整的依赖树(`go` 命令会写入 `go.mod`)。
所有文件的哈希值会与 `sum.golang.org` 上的已知哈希值进行比对,以防止标签被替换,并且它使用代理来防止仓库被“左填充”。
Go 模块还有其他方面的特性,包括安全功能,但本文不赘述。相关要点是:“依赖项通过 URL 标识,代码直接从 VCS 获取,并且对于直接和间接依赖都是如此”。
审计和更新依赖项很容易:我执行 `git log -p old..new`(通常通过 forge 的 Web 界面),阅读所有提交,然后更新 `go.mod` 文件。我的依赖项不多,而且已有的依赖项改动也不大。通常速度很快。我不需要做深入细致的代码审查,只需查找可疑内容。比如,在 glob 库中出现类似 `exec.Command(...)` 或 `http.Post(...)` 的代码就会很显眼。很难真正隐藏什么东西。
多年来,我一直对每个依赖项都这样做,作为一个独立开发者。这很简单。有些项目的依赖树大得多,这会变得更耗时,但并不难或令人困惑。仍然很简单,只是需要多一点时间。
---
对于 Ruby,情况不同,因为它有“发布包”这一步骤:你创建一个 `.gem` 归档并上传到 rubygems.org。你可以在里面放任何东西——无法保证 `.gem` 的内容与源码仓库一致。要进行审计,我需要做类似这样的事:
```
curl -s https://rubygems.org/downloads/example-2.7.5.gem >old.gem
curl -s https://rubygems.org/downloads/example-2.8.2.gem >new.gem
mkdir old new
tar xf old.gem -C old
tar xf new.gem -C new
(cd old && tar xf data.tar.gz)
(cd new && tar xf data.tar.gz)
diff -urN old new
```
这或许可行,但远非易事。单个提交丢失了,审计起来通常更困难。某些情况下 diff 足够小,还能接受。其他情况下 diff 很大,无法查看提交记录就很麻烦。我也完全能想象自己会搞混哪些审计过、哪些没有。我猜真正做这种审计的人非常少,因为它实在太麻烦了。
这并非 Ruby 独有的问题:这正是许多(大多数?)包系统的工作方式。
---
我见过的几乎所有“旁路攻击”也许更准确地描述为“包发布攻击”。它们依赖于在“发布包”步骤中注入某些东西。无论是 RubyGems、npm、PyPI、`.tar.gz` FTP 下载,还是其他什么,都是相对次要的细节。真正的源码仓库很少被攻破,因为那太显眼了。你的利用代码至少需要稍微隐藏一下才能有效。
最近的 npm 安全事件都依赖于获取 npm 账户权限并在已发布的包中注入某些内容。xz 在源码仓库中有一些利用代码,但处于休眠状态,隐藏在二进制测试文件中,仅在修改后的 `.tar.gz` 版本中才被激活。早在 2018 年,event-stream 添加了对 flatmap-stream 的依赖,而后者仅在发布的 npm 包中的 `index.min.js` 里包含恶意代码。
这指向了第二个问题:包含“编译后”资源的包。TypeScript 生成的 JavaScript 并非完全不可读,但肯定比原始 TypeScript 更难阅读和审计,更不用说压缩后的文件或二进制 blob 了。这在 Ruby 中问题较小,但在 npm 中则严重得多。
---
上周,RubyGems 添加了冷却选项(https://blog.rubygems.org/2026/06/03/cooldown-let-new-gems-be-vetted.html)和“针对最关键 gem 的 AI 辅助漏洞扫描”。作为短期措施,这并非坏事,但我认为更合适的解决方案是重新审视整个“发布包”模型。它缺乏所需的透明度。AI 工具不可能神奇地解决这个问题。
如果时光倒流二十年,我可能自己也会创建类似 RubyGems 的东西。在 SourceForge 上分发 `.tar.gz` 文件基本上是当时的工作方式,许多项目没有可公开访问的 VCS 仓库,或者根本没有。当时这通常运作得相当好。我并不是在责怪谁——我也会做同样的事。RubyGems 只是设计在一个不同的世界里。
我并不是说 RubyGems 和 npm 需要在每一个方面*完全*复制 Go 的做法,我也不是说 Go 的一切都完美无缺。但据我所知,这是迄今为止最好的方案。Go 模块的其他一些方面(如最小版本选择(https://go.dev/ref/mod#minimal-version-selection))则不那么重要。
我明白完全改变这种工作方式很困难,也可能造成破坏。但处理源源不断的被劫持包也同样困难和具有破坏性。所以……
在 Bundler 的具体情况下,已经*有一些*支持了。你可以这样做:
```
gem 'rails', git: 'https://github.com/rails/rails.git', tag: 'v8.1.2'
```
它会从 git 获取 rails gem,但仍然从 rubygems.org 获取依赖项。也许有办法让 Bundler 全部使用 git,但这是一场艰苦的战斗,而且很容易意外使用 rubygems.org。
这里只是随便想想,但类似下面的东西可能会大有帮助,并且不会破坏现有的 Gemfile:
```
# 禁止从 rubygems.org 获取任何内容;
# 改变 gem() 的行为,使其改用 git。
must 'use-git'
gem 'github.com/rails/rails', 'v8.1.2'
# 间接依赖(由 "bundle install" 等自动写入和更新)
indirect do
gem 'github.com/rails/actionview', 'v8.1.2'
gem 'github.com/fxn/zeitwerk', 'v2.8.2'
# ... 等等 ...
end
```
Gemfile.lock 仍然可以用于哈希值(类似于 go.sum),但除此之外用处不大。
这里有很多细节需要解决,我并不是说这些问题很容易妥善解决,但似乎确实存在一条合理的路径,我觉得?
---
或者也许完全不同的方案。我不知道。重点是:**我想像一个负责任的开发者那样可靠地审计我的依赖项,而 RubyGems 让这变得太难了**,其他包管理器也是如此(但我不关心它们)。
相似文章
GitHub 不应成为在 crates.io 上发布 Rust 的依赖条件
一篇讨论在 crates.io 上发布 Rust 包时对 GitHub 的依赖的文章,主张这种依赖不应是强制性的。
包管理器需要全局钩子
一篇博文,主张包管理器应支持全局钩子,作为当前包安全措施(如注册表或shell包装器)的更安全、更灵活的替代方案。
包管理器中的补丁与分支策略
本文探讨了在上游维护者未能解决漏洞时,针对不同语言包管理器修补和分支依赖项的策略。文章对比了系统包管理器强大的修补能力与语言注册表的局限性,并详细介绍了在各种生态系统中使用 Git 覆盖和分支等变通方法。
无法预防这种情况,唯一频繁发生此类事件的包管理器如此表示
讽刺文章指出npm注册表中反复出现的供应链攻击,与Go和Rust等更安全的生态系统形成对比,并嘲笑JavaScript社区对这种漏洞的接受态度。
@charliermarsh: 宣布 uv audit:原生支持扫描项目依赖中的漏洞
Charlie Marsh 宣布推出 uv audit,这是 uv 包管理器中用于项目依赖的原生漏洞扫描功能。