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 的 jobs | secrets 與 permissions 是否必要 |
| 不可信 PR 只跑測試 | pull_request | fork PR 不會取得一般 secrets,token 預設受限 |
| 測試完成後對 PR 留言或加標籤 | workflow_run | 後續流程不執行 PR 程式碼,只處理驗證過的資料 |
| 需要部署正式環境 | workflow_run + environment | environment 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-artifact、actions/github-script是否固定到經過審核的 major version。
如果要傳遞 build 結果,優先傳遞 hash、測試摘要或受限識別值;不要讓高權限 workflow 直接執行 artifact 裡的二進位檔、package.json script、Shell、Node 或 Docker 指令。
把權限與觸發條件縮到最小
workflow_run 不是自動成功條件。工作流完成時,不論前一個 workflow 成功或失敗,後續 workflow 都可能被觸發;要用 github.event.workflow_run.conclusion 判斷真正要執行的 job。除此之外,還要:
- 在 workflow 層級設定最小的
permissions,需要留言才給pull-requests: write。 - 把 deploy job 放到 environment,讓 required reviewers 成為第二道確認。
- 用
workflows、types、分支條件與 jobif限制觸發範圍。 - 不用
pull_request_target或workflow_run來執行 fork PR 的 build script。 - 下載 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 名稱、限制大小、驗證格式,並避免 source、npm install、node 或 Docker 直接使用未驗證內容。
Q: 什麼時候應改用 workflow_call?
A: 如果兩個流程都是同一份可信程式碼的一部分,而且可以明確宣告 inputs、secrets 與 permissions,workflow_call 通常更適合。workflow_run 適合需要在另一個 workflow 完成後取得不同權限上下文的情境,但也因此必須把它當成特權邊界設計。
參考資料:
GitHub Docs:Events that trigger workflows — workflow_run
回報錯字、失效連結,或告訴我你想看的延伸主題。