大家好,我是煎鱼。
前段时间有一起比较严重的安全事故,引起了国内外的集中关注、讨论和走查。听说个别朋友在当时都加塞了新的活,得加班加点检查一下。
这一连串事件,不禁让我思考到 Go 是如何解决和防护攻击相关的问题。周末翻到了《How Go Mitigates Supply Chain Attacks[1]》,内容物就是针对 Go 如何解决来自软件依赖的恶意攻击是相关的。基于此整理和调整后分享给大家。
安全事故:xz 核弹后门
这是今天内容的行业背景,如果没提前了解的同学可以看看。@ZainanZhou 大佬的对这件事的概述。
如下图:
图片
什么是供应链攻击
现代软件工程是协作的,并且基于开源软件(例如:GitHub)。这种做法使得目标容易受到供应链攻击,即通过破坏软件项目的依赖关系来进行攻击。
通过软件依赖项进行攻击
尽管有任何流程或技术措施,每个依赖关系都不可避免地存在信任关系的疑虑。在这一块领域,Go 工具做了许多的设计,有助于在不同阶段降低攻击的风险。
Go 对攻击的防护措施
所有构建都是 “锁定的”
外部世界的变化(例如:依赖项的新版本发布),不会自动影响 Go 的构建过程。
与其他大多数软件包管理器文件不同,Go 模块没有单独的约束列表和锁定特定版本的锁文件。任何 Go 构建所依赖的每个版本都完全由主模块的 go.mod 文件决定。
自 Go 1.16 版起,这种确定性被默认执行,如果 go.mod 文件不完整,编译命令(go build、go test、go install、go run 等)将会失败。
唯一会改变 go.mod(进而改变编译)的命令是:go get 和 go mod tidy。这些命令不会自动运行,也不会在 CI 中运行,需要人为控制。因此对依赖关系树的修改必须经过深思熟虑,并有机会通过代码审查发现。
这一点对安全性非常重要,因为当 CI 系统或新机器运行 go build 时,已签入的源代码是最终和完整的真实来源(即什么将被构建)。第三方无法对此产生影响。
此外,当使用 go get 添加依赖关系时,其传递依赖关系会按照依赖关系的 go.mod 文件中指定的版本添加,而不是按照它们的最新版本添加,这要归功于 go mod 的最小版本选择设计。
即使调用 go install example.com/cmd/devtoolx@latest也需要符合上述条件。在某些生态系统中,其配置的等效项会绕过固定版本。
在 Go 中,example.com/cmd/devtoolx 的最新版本将被获取,但所有依赖关系将由其 go.mod 文件设置版本控制。
也就是说,如果某个模块被入侵,并发布了新的恶意版本,在明确更新该依赖关系之前,任何人都不会受到影响,这就为生态系统提供了审查变更的机会和检测事件的时间。
版本内容永不改变
确保第三方无法影响构建所需的另一个关键属性是模块版本的内容不可更改。如果攻击者破坏了某个依赖项,并重新上传了现有版本,那么他们就会自动破坏所有依赖于该依赖项的项目。
图片
Kubernetes go.sum
这就是 go.sum 文件的作用。它包含了每个依赖项的加密哈希值列表,这些依赖项都对构建有贡献。并且 go.sum 文件不完整会导致运行错误。
现阶段 go.sum 文件只有 go get 和 go mod tidy 会修改它,因此对它的任何修改都会伴随着有意的依赖关系变更。其编译保证有完整的校验和。
有了 sumdb(Checksum Database,简称 sumdb),被破坏的依赖关系,甚至谷歌运营的 Go 基础架构都不可能用修改过(如被回溯过)的源代码来攻击特定的依赖关系。你所使用的代码与其他正在使用 example.com/modulex v1.9.2 并已审核的人所使用的代码完全相同。
VCS 是真相之源
大多数项目都是通过某种版本控制系统 (VCS) 开发的,然后在其他生态系统中上传到软件包仓库。
图片
最常见:git 和 svn
这意味着有两个账户可能被入侵,一个是 VCS 主机,另一个是软件包存储库,后者使用得更少,更容易被忽视。
这也意味着在上传到软件仓库的版本中更容易隐藏恶意代码,特别是如果源代码在上传过程中经常被修改,例如为了最小化恶意代码。
在 Go 中,不存在软件包版本库账户。软件包的导入路径包含 go mod 下载所需的信息,以便直接从 VCS 获取模块,而 VCS 中的标签定义了版本。
Go 模块存在一个镜像代理(例如:goproxy.cn),其在安全上有一个精妙之处:代理上的 go 工具在一个强大的沙箱中运行,并被配置为支持所有 VCS 工具,而默认情况下只支持两个主要的 VCS 系统(git 和 Mercurial)。
使用代理的用户仍然可以获取使用非默认 VCS 系统发布的代码,但在大多数安装中,攻击者无法获取这些代码。
构建代码不会执行代码
Go 工具链明确的安全设计目标是:无论是获取代码还是构建代码,都不会让代码执行,即使它是不受信任的恶意代码。
这一点与大多数其他生态系统不同,许多生态系统都能在获取软件包时运行代码,并且支持的很好。但现实上,黑客常常会将受攻击的依赖关系转化为受攻击的开发者机器,并对模块作者进行各种危险的攻击。
在 Go 中,不为特定模块进行构建和运行代码的话,不会对其产生安全影响。
少量复制胜过少量依赖
在 Go 生态系统中,最后也可能是最重要的软件供应链风险缓解措施是最没有技术含量的:Go 有一种拒绝大型依赖关系树的文化,这种文化倾向于少量复制,而不是添加新的依赖关系。
这可以追溯到 Go 的一个谚语:“a little copying is better than a little dependency[2]”。高质量的可重用 Go 模块自豪地贴上了 "零依赖" 的标签。
如果你发现自己需要一个库,你很可能发现它不会导致你依赖其他作者和所有者的数十个其他模块。
这也得益于丰富的标准库和附加模块(golang.org/x/......),它们提供了常用的高级构建模块,如 HTTP 栈、TLS 库、JSON 编码等。
这些意味着,只需少量的依赖关系,就能构建丰富、复杂的应用程序。无论多么优秀的工具都无法消除代码重用的风险,因此最有力的缓解措施永远是小型依赖关系树。
总结
Go 这一门编程语言背靠 Google,其在企业内部有一定规模的使用了他。因此 Go 在这几年中,Go 核心团队的目标之一就是缓解供应链的攻击,确保 Google 自身的软件体系较为安全可靠。
今天我们了解 Go 模块管理内的各种依赖库的管理机制等,其通过锁定构建的内容和版本、为 VCS 打造沙箱、构建工具的安全化等综合手段,为 Go 提供了保障护航。
参考资料
[1]How Go Mitigates Supply Chain Attacks: https://go.dev/blog/supply-chain
[2]a little copying is better than a little dependency: https://www.youtube.com/clip/UgkxWCEmMJFW0-TvSMzcMEAHZcpt2FsVXP65