我在 GitHub Actions 中运行的是谁的代码?
我在 GitHub Actions 中运行的是谁的代码?
- 标签:github, shell scripting
- 发布于 2025年3月25日
一周前,有人向 tj-actions/changed-files GitHub Action 添加了恶意代码。 如果您使用了被入侵的 action,它会将 secrets 泄露到您的构建日志中。 这些构建日志对于公共仓库是公开的,所以任何人都可以看到您的 secrets。 太可怕了!
可变引用 vs 不可变引用
这种攻击之所以成为可能,是因为在 GitHub Actions workflow 中引用 tags 是一种常见做法,例如:
jobs:
changed_files:
...
steps:
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v2
...
乍一看,这似乎是对已发布的 "version 2" 的 action 的不可变引用,但实际上这是一个可变的 Git tag。 如果有人更改了 tj-actions/changed-files
仓库中的 v2
tag,使其指向不同的 commit,那么下次运行此 action 时,将会运行不同的代码。
如果您指定一个 Git commit ID(例如 a5b3abf
),这是一个不可变的引用,每次都将运行相同的代码。
Tags vs commit IDs 是便利性和安全性之间的一种权衡。 指定确切的 commit ID 意味着代码不会意外更改,但 tags 更易于阅读和比较。
我是否有任何可变引用?
我并不担心这次特定的攻击,因为我没有使用 tj-actions
,但我很好奇我正在使用的其他 GitHub Actions 是什么。 我在所有仓库的本地克隆所在的文件夹中运行了一个简短的 shell 脚本:
find . -path '*/.github/workflows/*' -type f -name '*.yml' -print0 \
| xargs -0 grep --no-filename "uses:" \
| sed 's/\- uses:/uses:/g' \
| tr '"' ' ' \
| awk '{print $2}' \
| sed 's/\r//g' \
| sort \
| uniq --count \
| sort --numeric-sort
这会打印出我正在使用的所有 action 的计数。 这是输出的片段:
1 hashicorp/setup-terraform@v3
2 dtolnay/rust-toolchain@v1
2 taiki-e/create-gh-release-action@v1
2 taiki-e/upload-rust-binary-action@v1
4 actions/setup-python@v4
6 actions/cache@v4
9 ruby/setup-ruby@v1
31 actions/setup-python@v5
58 actions/checkout@v4
我浏览了整个列表,并考虑了我对每个 action 及其作者的信任程度。
- 是否来自像
actions
或ruby
这样的大型组织? 它们并不完美,但它们可能已经制定了良好的安全程序来防止恶意更改。 - 是否来自个人开发者或小型组织? 在这里,我往往更加谨慎,尤其是我不认识作者本人时。 这并不是说个人无法拥有良好的安全性,而是互联网上随机开发者的安全设置与大型组织之间存在更大的差异。
- 我是否需要使用别人的 action,或者我可以编写自己的脚本来替换它? 这通常是我更喜欢的,尤其是我只使用 action 提供的功能的一小部分时。 这在前期的工作量会稍大一些,但之后我就能确切地知道它在做什么,并且来自上游更改的变动和风险也会更小。
我对我的列表感觉很好。 我的大多数 action 来自大型组织,其余的是一些特定于我的 Rust 命令行工具的 action,这些工具是非关键的玩具,GitHub 仓库被入侵的影响相对较小。
此脚本的工作原理
这是 Unix pipelines 的一个经典用法,我将一堆内置的文本处理工具链接在一起。 让我们逐步了解它是如何工作的。
find . -path '*/.github/workflows/*' -type f -name '*.yml' -print0
这会查找任何 GitHub Actions workflow 文件——任何名称以 .yml
结尾的文件,位于像 .github/workflows/
这样的文件夹中。 它会打印一个文件名列表,例如:
./alexwlchan.net/.github/workflows/build_site.yml ./books.alexwlchan.net/.github/workflows/build_site.yml ./concurrently/.github/workflows/main.yml
它会使用一个空字节 (\0
) 将它们分隔开来打印,这使得在下一步中可以分割文件名。 默认情况下它使用换行符,但是空字节更安全一些,以防您有包含换行符的文件名。
我知道我总是使用 .yml
作为文件扩展名,但是如果您有时使用 .yaml
,您可以将 -name '*.yml'
替换为 \( -name '*.yml' -o -name '*.yaml' \)
。
我有很多本地仓库是开源项目的克隆,而不是我的代码,所以我不太关心它们正在使用的 GitHub Actions。 我通过添加额外的 -path
规则来排除它们,例如 -not -path './cpython/*'
.
xargs -0 grep --no-filename "uses:"
然后我们使用 xargs
逐个遍历文件名。 -0
标志告诉它以空字节分割,然后它运行 grep
来查找包含 "uses:"
的行——这是在您的 workflow 文件中使用 action 的方式。
--no-filename
选项意味着这只打印匹配的行,而不打印它来自的文件的名称。 不是我所有的文件都以一致的方式格式化或缩进,所以输出非常混乱:
- uses: actions/checkout@v4 uses: "actions/cache@v4" uses: ruby/setup-ruby@v1
sed 's/\- uses:/uses:/g' \
有时有一个前导连字符,有时没有——这取决于 uses:
是否是 YAML 字典中的第一个 key。 这个 sed
命令将 "- uses:"
替换为 "uses:"
,开始整理数据。
uses: actions/checkout@v4 uses: actions/cache@v4 uses: ruby/setup-ruby@v1
我知道 sed
是一个非常强大的工具,可以对文本进行更改,但我只知道几个简单的命令,比如这个替换文本的模式:sed 's/old/new/g'
。
tr '"' ' '
有时 action 的名称被引用,有时没有。 这个命令从输出中删除任何双引号。
uses: actions/checkout@v4 uses: actions/cache@v4 uses: ruby/setup-ruby@v1
现在我正在写这篇文章,我想到我也可以使用 sed
来进行这个替换。 我选择使用 tr
,因为我已经使用它很长时间了,并且对于进行单字符替换来说,语法更简单:tr '<oldchar>' '<newchar>'
awk '{print $2}'
这会在空格上分割字符串,并打印第二个 token,即 action 的名称:
actions/checkout@v4 actions/cache@v4 ruby/setup-ruby@v1
awk
是另一个强大的文本实用程序,我从未正确地学习过——我只知道如何在字符串中打印第 n 个单词。 它有很多我从未尝试过的模式匹配功能。
sed 's/\r//g'
我有一些 workflow 文件使用回车符 (\r
),并且这些文件包含在 awk
输出中。 这个命令去掉了它们,这使得数据对于最后一步更加一致。
sort | uniq --count | sort --numeric-sort
这会对行进行排序,以便相同的行相邻,然后它会分组并计算行数,最后重新排序,将最频繁的行放在底部。
我将此设置为一个名为 tally
的 shell alias。
6 actions/cache@v4 9 ruby/setup-ruby@v1 59 actions/checkout@v4
这种循序渐进的方法是我构建 Unix 文本 pipelines 的方式:我可以一次编写一个步骤,并逐步改进和调整输出,直到得到我想要的结果。 有很多方法可以做到这一点,并且因为这是一个我将使用一次然后丢弃的脚本,我不必太担心以“最纯粹”的方式来做——只要它能得到正确的结果,那就足够了。
如果您使用 GitHub Actions,您可能需要使用此脚本来检查您自己的 action,并查看您正在使用的 action。 但更重要的是,我建议您熟悉 Unix 文本处理工具和 pipelines——即使在人工智能时代,它们仍然是组合一次性脚本来处理数据的强大而灵活的方式。
这个网站由 Alex Chan (they/she) 用 ❤︎ 创建。 它以 CC BY 4.0 许可发布。
有疑问、意见或想说声谢谢? alex@alexwlchan.net
想要一份关于我正在做的事情的每月摘要?