打開網(wǎng)易新聞 查看精彩圖片

【CSDN 編者按】在自動(dòng)化 CI/CD 任務(wù)時(shí),GitHub Actions 為開發(fā)者提供了極大的便利。它允許我們輕松集成、測(cè)試和部署代碼,只需簡(jiǎn)單配置.github/workflows/即可調(diào)用社區(qū)或官方的 Actions 組件。但你有沒有想過——你在 GitHub Actions 里運(yùn)行的代碼,到底是誰的?它們真的安全嗎?

原文鏈接:https://alexwlchan.net/2025/github-actions-audit/

作者 | alexwlchan 翻譯|鄭麗媛

出品 | CSDN(ID:CSDNnews)

一周前,有人在 tj-actions/changed-files 這個(gè) GitHub Action 中植入了惡意代碼。如果你在工作流(workflow)中使用了這個(gè)受影響的 Action,它就會(huì)把你的敏感信息泄露到構(gòu)建日志中。而對(duì)于公共倉庫來說,這些構(gòu)建日志是公開的,任何人都能 看 到你的機(jī)密數(shù)據(jù)——這個(gè)漏洞相當(dāng)可怕!

(CSDN 付費(fèi)下載自視覺中國(guó))

可變引用 vs. 不可變引用

之所以會(huì)發(fā)生這樣的攻擊,是因?yàn)樵?GitHub Actions 的工作流中,通常會(huì)通過標(biāo)簽(tag)來引用某個(gè) Action,例如:

jobs:
  changed_files:
    ...
    steps:
      - name: Get changed files
        id: changed-files
        uses: tj-actions/changed-files@v2
      ...

乍一看,這似乎是一個(gè)對(duì) "v2" 版本的不可變引用,但實(shí)際上,v2 是一個(gè)可變的 Git 標(biāo)簽。如果有人在 tj-actions/changed-files 倉庫中更改了 v2 標(biāo)簽,使其指向不同的 commit,那么下次運(yùn)行時(shí),這個(gè) Action 執(zhí)行的代碼就會(huì)發(fā)生變化。

相比之下,如果你直接指定 Git 提交 ID(如 a5b3abf),那么這就是一個(gè)不可變引用,每次都會(huì)運(yùn)行相同的代碼。

總體而言,使用標(biāo)簽和提交 ID 之間存在一個(gè)權(quán)衡:

  • 指定確切的提交 ID 能確保代碼不會(huì)意外更改,可提高安全性。

  • 使用標(biāo)簽則更方便閱讀,也更容易與不同版本進(jìn)行比較。

我的工作流中,是否存在可變引用?

我對(duì)這次的攻擊并不擔(dān)心,因?yàn)槲覜]有使用 tj-actions,但我很好奇自己究竟使用了哪些 GitHub Actions。因此,我在本地克隆的所有倉庫中運(yùn)行了一段簡(jiǎn)短的 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

這個(gè)腳本會(huì)統(tǒng)計(jì)我使用的所有 GitHub Actions,并輸出了如下結(jié)果:

 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

于是,我仔細(xì)檢查了這份列表,并思考是否應(yīng)該信任每個(gè) Action 及其作者。

  • 這些 Action 來自像 actions 或 ruby 這樣的大型組織嗎?雖然它們并非完美無缺,但至少通常會(huì)有完善的安全措施來防范惡意更改。

  • 這些 Action 來自個(gè)人開發(fā)者或小型組織嗎?如果是的話,我會(huì)更加謹(jǐn)慎,尤其是當(dāng)我并不了解作者的時(shí)候。我并不是說個(gè)人開發(fā)者的安全性就一定不高,只是相比大型組織,互聯(lián)網(wǎng)上開發(fā)者們的安全性實(shí)在有些參差不齊。

  • 我是否真的需要使用別人的 Action? 或者,我能否自己編寫一個(gè)腳本來代替?通常情況下,我更傾向于自己寫腳本,特別是當(dāng)我只需要 Action 提供的部分功能時(shí)。雖然這樣做前期會(huì)多花一些功夫,但我可以完全掌控代碼,不必?fù)?dān)心上游發(fā)生變更而帶來的潛在風(fēng)險(xiǎn)。

總的來說,我對(duì)自己使用的 Actions 還算放心。大部分來自大型組織,其余的則是針對(duì)我的 Rust 命令行工具的一些特定 Actions,且這些工具都不是很關(guān)鍵,所以即使其 GitHub 倉庫被攻破,影響也不會(huì)太大。

這個(gè)腳本是如何工作的?

這是一個(gè)典型的 Unix 管道(pipeline)應(yīng)用,它串聯(lián)了多個(gè)內(nèi)置的文本處理工具來完成任務(wù)。下面,我們來逐步拆解它的工作方式。

find . -path '*/.github/workflows/*' -type f -name '*.yml' -print0 

這條 find 命令會(huì)查找所有 GitHub Actions 的工作流文件,即 .github/workflows/ 等文件夾中名稱以 .yml 結(jié)尾的文件,并打印出一個(gè)文件名列表,如:

./alexwlchan.net/.github/workflows/build_site.yml

./books.alexwlchan.net/.github/workflows/build_site.yml

./concurrently/.github/workflows/main.yml

在打印時(shí),文件名之間會(huì)有一個(gè)空字節(jié) (\0),以便在下一步中可以分割文件名。默認(rèn)情況下,它會(huì)使用換行符,但用空字節(jié)會(huì)更安全一些,以避免文件名中包含換行符時(shí)導(dǎo)致解析錯(cuò)誤。

如果你有時(shí)也把 .yaml 作為擴(kuò)展名,可以用 \( -name '*.yml' -o -name '*.yaml' \) 替換 -name '*.yml'。

我有很多本地 repos,它們都是開源項(xiàng)目的克隆,并不是我的代碼,所以我不太關(guān)心它們使用的是什么 GitHub Actions。我添加了額外的 -path 規(guī)則,如 -not -path './cpython/*' 將它們排除在外。

xargs -0 grep --no-filename "uses:" 

接下來,我們用 xargs 逐個(gè)處理文件名。-0 選項(xiàng)告訴 xargs 以空字節(jié)(\0)分割文件名,然后運(yùn)行 grep 查找包含 "uses:" 的行——這是在工作流文件中調(diào)用 Action 的方式。

-no-filename 選項(xiàng)意味著 grep 只輸出匹配的行,而不是文件名。由于我的文件格式和縮進(jìn)并不一致,因此輸出相當(dāng)混亂:

- uses: actions/checkout@v4

uses: "actions/cache@v4"

uses: ruby/setup-ruby@v1

sed 's/\- uses:/uses:/g' \ 

有時(shí) uses: 前面有一個(gè) -,有時(shí)沒有——這取決于 uses: 在 YAML 結(jié)構(gòu)中的位置。這個(gè) sed 命令會(huì)把"- uses:"替換為"uses:",以便整理數(shù)據(jù)。

uses: actions/checkout@v4

uses: "actions/cache@v4"

uses: ruby/setup-ruby@v1

我知道 sed 是一個(gè)功能強(qiáng)大的文本處理工具,但我只會(huì)用一些基本的命令,比如替換文本的模式:sed 's/old/new/g'。

tr '"' ' ' 

有些 Action 名稱會(huì)帶雙引號(hào),有些沒有,而這個(gè)命令則會(huì)移除輸出中的雙引號(hào)。

uses: actions/checkout@v4

uses: actions/cache@v4

uses: ruby/setup-ruby@v1

現(xiàn)在回過頭來看,我發(fā)現(xiàn)也可以用 sed 來完成這個(gè)替換。我之所以使用 tr,是因?yàn)槲覍?duì)它更熟悉,而且它的語法更簡(jiǎn)單,適用于單字符替換:tr ' ' ' '。

awk '{print $2}' 

這條命令會(huì)按空格拆分字符串,并打印第二個(gè)字段,也就是 Action 的名字:

actions/checkout@v4

actions/cache@v4

ruby/setup-ruby@v1

awk 是另一個(gè)強(qiáng)大的文本處理工具,但我我對(duì)它的了解不是很深——只知道如何打印字符串中的第 N 個(gè)單詞。其實(shí)它還支持很多模式匹配功能,不過我沒有嘗試過。

sed 's/\r//g' 

因?yàn)槲业囊恍┕ぷ髁魑募剀嚪╘r),這些字符會(huì)被 awk 處理后保留在輸出中。這條命令會(huì)去掉回車符,使數(shù)據(jù)在最終輸出時(shí)更加一致。

sort | uniq --count | sort --numeric-sort 

這部分命令會(huì)進(jìn)行排序和統(tǒng)計(jì),使相同的行相鄰,然后統(tǒng)計(jì)并計(jì)算每行的數(shù)量,最后重新排序,將出現(xiàn)頻率最高的行列放在最下面。

我將其作為一個(gè) shell 別名,名為 tally。

6 actions/cache@v4

9 ruby/setup-ruby@v1

59 actions/checkout@v4

這種循序漸進(jìn)的方法就是我構(gòu)建 Unix 文本管道的方法: 我可以一次寫一個(gè)步驟,逐步完善和調(diào)整輸出,直到得到我想要的結(jié)果。盡管有許多方法可以實(shí)現(xiàn)這一目標(biāo),但對(duì)于這種一次性的腳本來說,只要最終能得到正確的結(jié)果就夠了。

如果你使用 GitHub Actions,不妨用這個(gè)腳本檢查一下你自己的 Action 依賴項(xiàng),看看你正在使用哪些 Actions。除此之外,我強(qiáng)烈推薦你掌握 Unix 文本處理工具和管道——即使在 AI 時(shí)代,它們依然是處理數(shù)據(jù)的一種強(qiáng)大而靈活的方式,可以快速拼湊出一次性的數(shù)據(jù)處理腳本。