1709 字
9 分鐘

GitHub Actions workflow_run 安全怎麼設?先隔離 artifact 與 secrets

workflow_run 很適合把「不需要秘密的測試」和「需要寫入權限的留言、標籤或部署」拆成兩個 workflow。但它有一個容易被忽略的特性:後續 workflow 即使前一個 workflow 沒有 secrets 或 write token,也可能取得 secrets 與可寫入的 GITHUB_TOKEN

直接原則是:workflow_run 當成特權邊界;後續流程不要 checkout 不可信的 PR 分支,也不要直接執行前一個 workflow 上傳的 artifact。artifact 只能當成資料下載到暫存目錄,再做格式驗證,最後用最小 permissions 完成單一動作。

為什麼 workflow_run 不是普通的工作流串接?#

GitHub 官方文件把 workflow_run 定義為在另一個 workflow 被要求、執行中或完成時觸發。它能存取 secrets 和 write token,這正是它可以在低權限 CI 後執行留言或部署的原因,也正是風險所在。

需求合適的邊界先確認什麼
同一份可信程式碼拆成多個工作workflow_call 或同一 workflow 的 jobssecrets 與 permissions 是否必要
不可信 PR 只跑測試pull_requestfork PR 不會取得一般 secrets,token 預設受限
測試完成後對 PR 留言或加標籤workflow_run後續流程不執行 PR 程式碼,只處理驗證過的資料
需要部署正式環境workflow_run + environmentenvironment approval、分支與 artifact 來源

這和 GitHub Actions pull_request_target 的 checkout 保護是相關但不同的問題:pull_request_target 直接在另一個事件中取得 base repository 的權限;workflow_run 則是前一個 workflow 完成後開啟一個新的權限上下文。兩者都不能把不可信程式碼帶進高權限步驟。

安全結構:第一個 workflow 只測試,第二個只處理資料#

第一個 workflow 可以在 pull_request 上建置和測試,並把必要的識別資料存成 artifact。這裡的檔案內容不是可信程式碼;後續 workflow 仍要把它當成攻擊者可控制的輸入:

name: Untrusted CI
on:
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: pnpm install --frozen-lockfile
- run: pnpm test
- name: Save PR number as data
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
mkdir -p pr-metadata
printf '%s\n' "$PR_NUMBER" > pr-metadata/pr-number
- uses: actions/upload-artifact@v4
with:
name: pr-metadata
path: pr-metadata/pr-number

真正的安全邊界在第二個 workflow:它不 checkout repository、不把 artifact 目錄加入 PATH,也不把檔案內容直接拼進 shell 指令。它只讀取一個預期為正整數的 PR 編號,再呼叫 GitHub API。

$RUNNER_TEMP 下載並驗證 artifact#

下面的範例只在前一個 workflow 成功且事件類型為 pull_request 時留言。actions/download-artifact 將內容放在 runner 的暫存目錄;即使檔案看起來像 shell script,也不會被執行。

name: Comment after trusted validation
on:
workflow_run:
workflows: [Untrusted CI]
types: [completed]
permissions:
actions: read
pull-requests: write
jobs:
comment:
if: >-
${{ github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'pull_request' }}
runs-on: ubuntu-latest
steps:
- name: Download metadata only
uses: actions/download-artifact@v5
with:
name: pr-metadata
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: ${{ runner.temp }}/pr-metadata
- name: Validate PR number
id: metadata
shell: bash
run: |
set -euo pipefail
file="${RUNNER_TEMP}/pr-metadata/pr-number"
test -s "$file"
pr_number="$(tr -d '[:space:]' < "$file")"
case "$pr_number" in
''|*[!0-9]*)
echo 'Invalid PR number' >&2
exit 1
;;
esac
printf 'pr_number=%s\n' "$pr_number" >> "$GITHUB_OUTPUT"
- name: Comment through the API
uses: actions/github-script@v8
with:
script: |
const issueNumber = Number('${{ steps.metadata.outputs.pr_number }}');
if (!Number.isSafeInteger(issueNumber) || issueNumber < 1) {
core.setFailed('The PR number is outside the expected range.');
return;
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: 'The trusted follow-up workflow completed its validation.'
});

這個範例刻意沒有 actions/checkout。如果後續工作真的需要讀取程式碼,應改成 checkout base repository 的可信 ref,並重新評估是否能把那一步移回第一個低權限 workflow;不要直接 checkout github.event.workflow_run.head_sha 後執行其中的 script。

artifact 不是可信的設定檔#

GitHub 文件的 workflow_run 範例會下載前一個 workflow 的 artifact,再用它取得 PR 編號。這不代表任何 artifact 都能直接信任。實際設計時至少檢查:

  • artifact 名稱是否固定,是否可能被不可信流程替換成另一個檔案集合;
  • 下載路徑是否在 $RUNNER_TEMP,而不是直接展開到工作目錄;
  • 檔案格式是否有明確 schema,例如只接受正整數或固定 JSON 欄位;
  • 是否拒絕換行、shell metacharacter、超大檔案與多餘檔案;
  • 後續命令是否把驗證後的值當參數傳入,而不是拼成新的 shell script;
  • actions/download-artifactactions/github-script 是否固定到經過審核的 major version。

如果要傳遞 build 結果,優先傳遞 hash、測試摘要或受限識別值;不要讓高權限 workflow 直接執行 artifact 裡的二進位檔、package.json script、Shell、Node 或 Docker 指令。

把權限與觸發條件縮到最小#

workflow_run 不是自動成功條件。工作流完成時,不論前一個 workflow 成功或失敗,後續 workflow 都可能被觸發;要用 github.event.workflow_run.conclusion 判斷真正要執行的 job。除此之外,還要:

  1. 在 workflow 層級設定最小的 permissions,需要留言才給 pull-requests: write
  2. 把 deploy job 放到 environment,讓 required reviewers 成為第二道確認。
  3. workflowstypes、分支條件與 job if 限制觸發範圍。
  4. 不用 pull_request_targetworkflow_run 來執行 fork PR 的 build script。
  5. 下載 artifact 後先驗證,再做 API 呼叫;不要以「artifact 是自己上傳的」作為信任判斷。

若工作流只是同一個 repository 內的可信 jobs 拆分,workflow_call 通常更容易追蹤輸入與 permissions;若只需要測試結果,不需要 secrets 或寫入權限,留在 pull_request workflow 會更直觀。

結論:先決定「誰能執行程式碼」,再決定是否用 workflow_run#

workflow_run 的價值是把低權限檢查和高權限副作用分開,不是讓第二個 workflow 接手第一個 workflow 的完整工作目錄。只要把 artifact 當不可信資料、把下載位置放在暫存目錄、把輸入限制成可驗證的值,並把 permissions 與 environment approval 寫清楚,才有可能安全地使用這個事件。

常見問題#

Q: workflow_run 觸發的 workflow 可以 checkout 前一個 PR 的 commit 嗎?#

A: 技術上可以指定 ref,但 GitHub 官方警告不要在 workflow_run 上執行不可信的 PR 程式碼。新的 workflow 可能有 secrets 與 write token;若 checkout 後執行 script,攻擊者就可能把它變成權限提升路徑。需要程式碼測試時,應留在低權限的 pull_request workflow。

Q: 只把 artifact 下載到暫存目錄就安全了嗎?#

A: 不足以單獨保證安全。暫存目錄能降低誤把檔案當工作目錄執行的機率,但仍要固定 artifact 名稱、限制大小、驗證格式,並避免 sourcenpm installnode 或 Docker 直接使用未驗證內容。

Q: 什麼時候應改用 workflow_call#

A: 如果兩個流程都是同一份可信程式碼的一部分,而且可以明確宣告 inputs、secrets 與 permissions,workflow_call 通常更適合。workflow_run 適合需要在另一個 workflow 完成後取得不同權限上下文的情境,但也因此必須把它當成特權邊界設計。

參考資料:

GitHub Docs:Events that trigger workflows — workflow_run

GitHub Docs:Secure use reference

GitHub Docs:Using artifacts in a workflow

GitHub Actions workflow_run 安全怎麼設?先隔離 artifact 與 secrets
https://laplusda.com/posts/github-actions-workflow-run-artifact-security/
作者
Zero
發佈於
2026-08-19
許可協議
CC BY-NC-SA 4.0
這篇文章有幫助嗎?

回報錯字、失效連結,或告訴我你想看的延伸主題。