依赖项应直接从版本控制系统获取
速览
传统上依赖项通过包管理器从公共仓库获取,但存在安全风险。新建议主张从VCS直接拉取,可确保来源可靠并减少中间环节,有望推动开发流程变革,对软件供应链安全有积极意义。
AI 深度解读
背景
本文作者是一位近年来主要使用 Go 的开发者,最近在新的工作中重新开始写 Ruby。在对比两种语言的体验后,作者认为 Go 在依赖管理——尤其是安全方面——明显优于 Ruby。文章指出,当前许多包管理系统(如 RubyGems、npm、PyPI)都采用“发布包”的模式,即开发者将代码打包成 .gem、.tgz 等制品上传到中央仓库,这种模式存在严重的透明度和审计问题。作者主张将依赖直接通过版本控制系统(VCS)获取,就像 Go Modules 所做的那样,并以此作为提高软件供应链安全性的根本途径。
核心内容
Go 的依赖管理机制不依赖“发布包”这一步骤。依赖由 URL 标识,例如 github.com/user/pkg。Go 会自动识别所使用的 VCS(通常是 Git),然后直接从仓库中获取你在 go.mod 文件中指定的标签或提交哈希。go.mod 文件同时扮演了依赖声明和锁定文件的角色,它列出精确的版本号(不支持 ~>1.1 这样的范围约束),并且包含直接依赖和间接依赖,形成完整的依赖树(go 命令会自动写入 go.mod)。Go 还会通过 sum.golang.org 检查所有文件的哈希值,防止标签被篡改;同时使用代理以避免因仓库被删除或改名而导致的“左移”攻击。
对于作者来说,审计和更新 Go 依赖非常容易:只需执行 git log -p old..new(通常通过代码托管平台的 Web UI),阅读所有提交,然后更新 go.mod。作者的依赖数量不多且变化不大,审计过程通常很快。他不需要进行深度审查,只需留意可疑内容,比如一个 globbing 库中出现了 exec.Command(...) 或 http.Post(...) 调用——这类异常很难隐藏。
相比之下,Ruby 的依赖管理完全不同:它要求开发者创建 .gem 归档文件并上传到 rubygems.org。上传的 .gem 内容与源代码仓库之间没有保证对应关系。要审计依赖更新,作者需要手动下载两个版本的 .gem 文件,解压,然后进行 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 增加了“冷却期”选项和“针对最关键的 gem 的 AI 辅助漏洞扫描”。作者认为,作为短期措施这并非坏事,但更根本的解决方案应该是重新审视整个“发布包”模型。这种模式缺乏必要的透明度,AI 工具不可能神奇地解决这个问题。
作者承认,二十年前他自己也可能会创建类似 RubyGems 的东西——当时在 SourceForge 上分发 .tar.gz 是主流做法,许多项目没有公开的 VCS 仓库,甚至根本没有 VCS。这在当时是合理的。RubyGems 只是诞生于一个不同的世界。
作者并不主张 RubyGems 和 npm 完全照搬 Go 的一切,也不认为 Go 的方案完美无缺。但就目前所知,Go 的依赖管理模式是最好的。Go Modules 的其他方面(如最小版本选择)则不那么重要。
作者理解彻底改变现有机制是困难且可能具有破坏性的,但应对无休止的劫持包同样困难且具有破坏性。
Bundler 已经有部分支持:可以指定 gem 'rails', git: 'https://github.com/rails/rails.git', tag: 'v8.1.2',这样会从 Git 获取 Rails,但其他依赖仍会从 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(以及其他包管理器)让这件事变得过于困难。
关键要点
- Go 的依赖管理优势:依赖由 URL 标识,直接从 VCS 获取,无需“发布包”步骤;
go.mod精确记录版本和完整依赖树;通过sum.golang.org进行哈希校验,防止篡改。 - RubyGems 的审计困难:需要手动下载、解压、diff
.gem包,丢失提交历史;审计流程繁琐且易混淆,导致很少有人真正去做。 - “发布包”模式是安全漏洞的根源:大多数“侧信道攻击”本质上是“包发布攻击”,攻击者利用上传步骤注入恶意代码,而源代码仓库本身很少被直接攻破。
- 编译/压缩资源加剧问题:TypeScript、被压缩的 JavaScript 或二进制 blob 降低了可审计性,npm 生态尤其严重。
- 短期措施治标不治本:AI 辅助扫描和冷却期虽然有用,但无法解决透明度缺失的根本问题。
- 可行的改进方向:引入类似
must 'use-git'的声明,强制所有依赖通过 VCS 获取,并自动记录间接依赖,类似于 Go 的做法。
意义与影响
这篇文章从一个资深开发者的实践视角,直击了当前软件供应链安全的核心痛点:包管理器“发布包”模式带来的透明度和审计困难。
