2025年3月28日にあったtj-actionやreviewdogのセキュリティインシデントCVE-2025-30066を受けて、GitHub Actionsワークフローの書き方が変えてしばらく経ちました。その対応メモを残します。
- CVE-2025-30066の概要
- CVE-2025-30066の影響を軽減するにはどうすればよいのか
- 静的解析ツールと自動化
- GitHubユーザー/オーガニゼーションの設定
- GitHubリポジトリごとの設定
- まとめ
- 参考
CVE-2025-30066の概要
CVE-2025-30066の詳細はセキュリティインシデントに詳しいですが、簡単に説明するとspotbugs/sonar-findbugsを起点にreviewdog/action-setupを経由してtj-action/changed-filesのリリースのほとんどが改ざんされました。改ざんコミットは、base64エンコードされたPythonのエクスプロイトコードを取得/実行するもので、GitHub Actionsのランナーで実行されるとアクションシークレットを取得してBASE64エンコーディングしてログ出力します。攻撃者の狙いはcoinbase/agentkitだったようですが、tj-action/changed-filesの利用者が多く、影響範囲が広がりました。
ユーザーインパクトは、tj-action/changed-filesをタグ指定で利用しているGitHub ActionsワークフローでアクションシークレットがActionログに出力されます。パブリックリポジトリはビルドログを第三者が確認できるため、GitHub Actionsのシークレットが漏洩します。プライベートリポジトリはCIログを外部から確認できないため影響は限定的です。
なお、CVE発生時にGitHubは次の対応をしています。プラットフォーマーのアクションも興味深いです。
- tj-action/changed-filesのリポジトリ/アクションを削除してワークフロー使用できないように変更
- アクションのすべてのリリースがクリーンアップ、悪意あるコードが含まれなくなってからリポジトリを復旧
- 悪意のあるユーザーjurkaofavak、randolzfowの削除
- Gistに公開されていたエクスプロイトで利用されたPythonコードの削除1
- 各ユーザーのGitHub PATを期限切れに設定 (各ユーザーはPATを発行しなおしたはずです)
CVE-2025-30066の影響を軽減するにはどうすればよいのか
CVE-2025-30066のようなGitHub Actionsのインシデントは今後も起こる可能性が高いです。GitHub Actionsはオープンソースであり誰でもアクションを作成・コントリビュートできますし、メンテナのPATが漏洩して意図せず変更される可能性もあるでしょう。私が利用者として改めて認識したポイントは次の通りです。
- アクションのタグ/リリースは書き換えられる可能性がある
- 利用者のPersonal Access Token (PAT)やGITHUB Appはアクションで漏れる可能性がある
- ワークフローやジョブに設定した環境変数/シークレットはアクションを使ってCIログに書き出せる
- ワークフローやジョブの権限が高いほどインパクトが大きい
- パブリックリポジトリの
pull_request_target
イベントの利用はリスクが高い - コミットは別ユーザーで偽れる
- 利用できるアクションを限定するのはある程度有効
ポストモーテムで得られる教訓を並べてみましょう。このうちA, C, D, F, Gは静的解析ツールで自動検知できるので優先度高めに対応します。Bはユーザー/オーガニゼーションで一度設定すればOKそうです。E, H, I,Jはリポジトリごとに設定が必要です。Kはアクションを採用するときの判断基準です。
- A: アクションはタグ/リリースではなくコミットSHAを指定する (pinactで対応可能)
- B: ワークフローに指定するPATやGITHUB Appは権限を最小限にする
- C: シークレットを環境変数に設定する場合、ステップの環境変数に設定する (ghalintで検出可能)
- D: ジョブごとに最小権限を設定する (ghalintで検出可能)
- E: リポジトリの自動アクショントークンのデフォルト権限を
write
からread
に設定2 - F: パブリックリポジトリで
pull_request_target
イベントは極力避ける (ghalint/zizmorで検出可能) - G: ワークフローの記法をそろえる (actionlintで検出可能)
- H: Rulesetでコミット署名の必須化を検討する
- I: Rulesetでタグの更新・削除を禁止
- J: Rulesetでタグの作成可能なユーザーをorganization adminやrepository adminに限定する
- K: アクションの利用前に処理内容を把握、管理する
静的解析ツールで設定の確認/調整/自動化、次いでユーザー/オーガニゼーションの設定見直し、リポジトリごとの設定、という順番で対応してみましょう。
私は今回のCVE経過中の対応を見て、tj-actionとreviewdogは使わないことにしました。
静的解析ツールと自動化
真っ先に取り組むべきは静的解析ツールによる自動化です。リポジトリを複数管理していると手動でワークフローを調整するのは無理です。時間は有限なので、できるだけ自動化して見落としを防ぎつつ精度を高めましょう。ツールには静的解析ツールとセキュリティプラクティスをワークフローに自動適用するヘルパーツールがあります。
使っているワークフローの静的解析ツールは次の通りです。これだけだと指摘が多すぎると対応が困難になるため、ワークフローを自動修正するヘルパーツールも併用します。
ツール名 | 説明 |
---|---|
actionlint | ワークフローの構文チェックとShellcheckなど |
ghalint | ワークフローの簡易的なセキュリティチェック |
zizmor | ワークフローのより厳しいセキュリティチェック |
セキュリティプラクティスをワークフローに自動適用するヘルパーツールは、静的解析で指摘されるいくつかを修正してくれます。残念ながら限定的な修正ですが、まずはこれを実行してから静的解析して修正するかどうか決めるといいでしょう。
ツール名 | 説明 |
---|---|
pinact | タグやブランチ指定されたアクションをSHAに変換する |
disable-checkout-persist-credentials | checkoutアクションのpersist-credentials を無効化する |
私は次の流れで自動化を進めました。順に見ていきましょう。
- pinactでSHAに変換する
- disable-checkout-persist-credentialsで
checkout
アクションのpersist-credentials
を無効化する - Dependabotでアクション更新を自動化
- actionlintを実行してワークフロー構文を標準化する
- ghalintでセキュリティチェックを実行して、出てきたエラー/警告を修正する
- (Optional) zizmorでセキュリティチェックを実行して、出てきたエラー/警告を修正する
- GitHub Actionsで静的解析を自動化する
アクションをSHAで固定する(pinact)
pinactはGitHub ActionsのタグやブランチをSHAに一括変換するツールです。SHAに変換することで、今回のインシデントのようにタグに悪意のあるコードが仕込まれた場合でも影響を受けません。3
後述するghalint
でSHA指定していないのを検出できるのですが、pinact
コマンドを叩くだけで修正できます。
$ pinact run
嬉しいポイント
コマンドを実行するだけでSHAに変換されるので、手作業より圧倒的に簡単、確実です。SHAにしたいならpinact
で99%解決します。コンフィグファイルもありますが、おおむね不要でしょう。
# ❌: tag指定 - uses: actions/checkout@v4 # ⭕: SHA指定 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
ドキュメントもアップデート対象にできますが、これはうまくいかないケースもあるようなのでうまくいったら便利程度でいいでしょう。
つらいポイント
Organization内部で常にブランチを見たいケースでSHAに変換されるのは邪魔です。先ほどの1%に該当するので都度対処でいいです。
設定ファイル.pinact.yaml
でignore_actions
に指定するとSHAに変換されないようにできます。
# 意図的にブランチを指定したいケースでSHAに変換されると面倒 - uses: foo-organization/bar-action@main
actions/checkoutのpersist-credentialsをfalseに変換する(disable-checkout-persist-credentials)
GitHub Actionsでチェックアウトするときはactions/checkout
を使うのが定番です。ただデフォルトでpersist-credentials: true
になっており、悪意あるアクションがリポジトリにコミット障壁がpermission
だけです。明示的にpersist-credentials: false
を設定することで、チェックアウト後のGit認証情報を無効してGit認証情報にアクセスできるステップを限定できます。
後述するghalint
やzizmor
でfalseにしていないのを検出できるのですが、disable-checkout-persist-credentials
コマンドを叩くだけで修正できます。
$ disable-checkout-persist-credentials
嬉しいポイント
コマンドを実行するだけでpersist-credentials: false
が設定されるので、手作業より圧倒的に簡単、確実です。
# ❌: persist-credentialsがtrueだとghalintでエラーが出る - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: true # ❌: persist-credentialsが無指定(true相当)だとghalintとzizmorでエラーが出る - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # ⭕: persist-credentialsがfalseを指定する - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false
つらいポイント
persist-credentials: false
にすると、後続のステップでプッシュできなくなります。地味にめんどくさいですが定形パターンで対応できます。
fatal: could not read Username for 'https://github.com': No such device or address Failed to push, try 'git pull --rebase' to resolve ...
GitHub Actionsでプッシュするには、git remote set-url
で認証付きリモートURLを設定します。コミット前にリモートリポジトリ情報をセットしてあげればpushできるようになります。
# ❌: git pushができない - name: git push run: git push # ⭕: コミット前にリモートリポジトリ情報をセットする - name: update current git to latest & Push changes shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git remote set-url origin "https://github-actions:${GITHUB_TOKEN}@githb.com/${{ github.repository }}" git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - name: git push run: git push
Actionsの自動更新を行う(Dependabot)
リポジトリにDependabot4の設定ファイル.github/dependabot.yaml
を用意しておくとDependabotによるGitHub Actionsのバージョン更新が自動化できます。幸いにしてSHAで指定した場合でもDependabotはSHAを更新してくれます。
# 元がSHA指定でも - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 # Dependabotは更新してくれる - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
更新頻度を調整する
頻繁すぎる更新は好ましくないと考えているので、週一更新かつパッチバージョンは更新しないようにしています。普段使っている設定は次の通りです。
version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" # Check for updates to GitHub Actions every week ignore: # I just want update action when major/minor version is updated. patch updates are too noisy. - dependency-name: '*' update-types: - version-update:semver-patch
アクションの静的解析を行う(actionlint)
ここからは静的解析ツールです。
actionlintはGitHub Actionsのワークフロー/アクションの構文チェックを行うツールです。構文チェックだけでなく、Shellcheckやpyflakeなども実行してくれます。YAMLの書き方が人それぞれなのはしょうがないですが、actionlintを使うことで書き方をある程度揃えられます。
actionlintを使うと、次のような状況を改善できます。
- GitHub Actionsのワークフローの妥当性検出
- GitHub Actionsの
${{ }}
構文の妥当性を検出 runs-on
に指定された不明なランナーを検出github
コンテキストでリスクが高いパラメーターをrunsに直接埋め込んでいると検出- Composite Actcionsのシェルがないと検出
- 他
実行も簡単です。actionlintが何かしら検出すると終了コードが1になるので、CI/CDで使いやすいです。後述しますが、私はCI/CDでactionlintを用いています。
$ actionlint
# ❌: runs-onがないのでエラー jobs: dump-context: timeout-minutes: 5 steps: - run: echo foo # ⭕: runs-onを指定する jobs: dump-context: timeout-minutes: 5 runs-on: ubuntu-24.04 steps: - run: echo foo
除外ルール
なかにはルールを除外したい場合もあるでしょう。.github/actionlint.yaml
に除外ルールを書くことで検出から除外できます。特に長年shellcheckはコメントでしか除外できなかったのが改善して最高です。
例えば次のstepを用意します。
- name: action run: echo $GITHUB_ACTION env: GITHUB_ACTION: ${{ github.action }}
actionlintを実行すると、shellcheckのSC2086に引っかかります。
$ actionlint .github\workflows\context-github.yaml:53:9: shellcheck reported issue in this script: SC2086:info:1:6: Double quote to prevent globbing and word splitting [shellcheck] | 53 | run: echo $GITHUB_ACTION | ^~~~
SC2086は有益なことも多いのですが微妙なことも多いので、.github/actionlint.yaml
に除外ルールを設定しておきます。これで引っかからなくなります。
paths: .github/workflows/**/*.{yml,yaml}: ignore: - 'shellcheck reported issue in this script: SC2086:.+'
嬉しいポイント
actionlintを使う最大のメリットは、記法が一定になることです。例えばGitHub Actionsの構文はif: 式
とif: ${{ 式 }}
の2つの書き方があります。actionlintを使うことで後者に統一されるので、VS Codeの拡張でも式がコードハイライトされるのでよいです。
記法の統一は、GitHub Actionsをチームでハンドリングするにあたって地味に大事です。
つらいポイント
shellcheckを除外するのが正直面倒です。例えば$GITHUB_ENV
とかも"$GITHUB_ENV"
のようにダブルクォートで指定する必要があるので、除外ルールに書きたくなります。
actionlint自体の良さと、bash自体に疲弊するバランスが難しい。というか、shellcheck
が割とやりすぎている感じがあってactionlintの印象がshellcheckに引っ張られている気がします。
アクションのセキュリティチェックを行う(ghalint)
ghalintはGitHub Actionsのワークフロー/アクションの簡易的なセキュリティチェックを行うツールです。 一番気になるところをカバーしてくれ、Goで書かれていてOS問わず動かしやすいため、まず検討するのがおすすめです。 野良のワークフローはほぼghalintの指摘をできていないので、どこを見るといいか目を養えます。
ghalintは次のようなことを検出してくれます。
- permissionsの指定をしていないと検出
read-all
やwrite-all
の過剰な権限を検出- アクションの指定をフルSHAにしていないと検出
persist-credentials: false
がないactions/checkoutを検出secrets: inherit
を検出- secretsのワークフローレベル環境偏すへの設定を検出
- secretsのジョブレベル環境変数への設定を検出
- コンテナジョブのイメージにlatestを指定していると検出
- GitHub Appをリポジトリ限定していないと検出
- ジョブのタイムアウト指定がないと検出
- 他
実行も簡単です。ghalintが何かしら検出すると終了コードが1になるので、CI/CDで使いやすいです。後述しますが、私はCI/CDでghalintを用いています。
# ワークフローへのghalint実行 $ ghalint run # アクションへのghalint実行 $ ghalint run-action
例えば次のようなワークフローはghalintで検出されます。
name: foo bar on: push: branches: ["main"] # ❌: permissionsがジョブレベルにないのでエラー (複数ジョブの時だけ検出) permissions: contents: read # ❌: secretsをワークフローレベルの環境変数に指定しているのでエラー env: BAR: ${{ secrets.BAR }} jobs: action: # ❌: secretsをジョブレベルの環境変数に指定しているのでエラー env: FOO: ${{ secrets.FOO }} runs-on: ubuntu-24.04 # ❌: timeout-minutesがないのでエラー steps: # ❌: actions/checkoutがSHA指定されていないのでエラー # ❌: persist-credentials=falseがないのでエラー - uses: actions/checkout@v4.2.2 - run: echo "FOO=$FOO" action-2: runs-on: ubuntu-24.04 timeout-minutes: 3 steps: - run: echo "BAR=$BAR"
メッセージにエラー原因や修正方法が書かれているので従いましょう。
name: foo bar on: push: branches: ["main"] jobs: action: # ⭕: permissionsをジョブレベルに指定する permissions: contents: read runs-on: ubuntu-24.04 # ⭕: timeout-minutesを指定する timeout-minutes: 3 steps: # ⭕: actions/checkoutをSHA指定する # ⭕: persist-credentials=falseを指定する - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false # ⭕: secretsをステップレベルの環境変数に指定する - run: echo "FOO=$FOO" env: FOO: ${{ secrets.FOO }} action-2: # ⭕: permissionsをジョブレベルに指定する permissions: contents: read runs-on: ubuntu-24.04 timeout-minutes: 3 steps: # ⭕: secretsをステップレベルの環境変数に指定する - run: echo "BAR=$BAR" env: BAR: ${{ secrets.BAR }}
除外ルール
なかにはルールを除外したい場合もあるでしょう。.ghalint.yaml
に除外ルールを書くことでルール検出から除外できます。検出はエラーメッセージのログから作ることができて便利です。
例えば次のようなワークフローを用意します。自分のリポジトリのワークフローなのでブランチ指定を許可したいのですが、SHA指定じゃないので当然エラーになります。また自リポジトリのReusable Workflowはsecrets: inherit
を許可したいのですが、inherit
はエラーになります。
jobs: call-workflow-passing-data: permissions: contents: read uses: guitarrapc/githubactions-lab/.github/workflows/_reusable-workflow-called.yaml@main # ghalintでエラーになる call-workflow-passing-data2: permissions: contents: read uses: ./.github/workflows/_reusable-workflow-nest.yaml with: username: "foo" is-valid: true secrets: inherit # ghalintでエラーになる
ghalintを実行すると次のようなエラーが出ます。action_ref_should_be_full_length_commit_sha
ポリシーに引っかかっているのが分かります。
$ ghalint run ERRO[0000] the job violates policies error="`secrets: inherit` should not be used. Only required secrets should be passed explicitly" job_name=call-workflow-passing-data2 policy_name=deny_inherit_secrets program=ghalint reference="https://github.com/suzuki-shunsuke/ghalint/blob/main/docs/policies/004.md" version=1.2.3 workflow_file_path=".github\\workflows\\reusable-workflow-public-caller.yaml" ERRO[0000] the job violates policies action=guitarrapc/githubactions-lab/.github/workflows/_reusable-workflow-called.yaml error="action ref should be full length SHA1" job_name=call-workflow-passing-data policy_name=action_ref_should_be_full_length_commit_sha program=ghalint reference="https://github.com/suzuki-shunsuke/ghalint/blob/main/docs/policies/008.md" version=1.2.3 workflow_file_path=".github\\workflows\\reusable-workflow-public-caller.yaml"
自分のリポジトリのactionは除外したいので、.ghalint.yaml
に除外ルールを書きましょう。action_ref_should_be_full_length_commit_sha
はaction_name
を求めるのに注意です。ワイルドカードを指定できるのでaction_name
でワイルドカード指定、workflow_file_path
を省略すれば自分のリポジトリのReusable Workflowをまとめて除外もできます。
excludes: - policy_name: action_ref_should_be_full_length_commit_sha action_name: guitarrapc/githubactions-lab/.github/workflows/* workflow_file_path: .github/workflows/reusable-workflow-public-caller.yaml - policy_name: deny_inherit_secrets workflow_file_path: .github/workflows/reusable-workflow-public-caller.yaml job_name: call-workflow-passing-data2
嬉しいポイント
ghalintを使うことで、これはまずいのか、まずくないのか、という部分が定形的に検出されて修正する方向に力が働きます。指摘の程度もあまりに厳しいものはないので、使い勝手のバランスがいいです。
再利用可能なワークフローでsecrets: inherit
をエラーにするのは割といい指摘に感じます。ついやっちゃいがちですが、パブリックなリポジトリではsecretsをちゃんと明示するほうがいいでしょう。
# ❌: secretsをinheritにするとエラー jobs: call-workflow-passing-data: uses: ./.github/workflows/_reusable-workflow-called.yaml with: username: ${{ inputs.username }} is-valid: ${{ inputs.is-valid }} secrets: inherit # ⭕: secretsを明示する jobs: call-workflow-passing-data: uses: ./.github/workflows/_reusable-workflow-called.yaml with: username: ${{ inputs.username }} is-valid: ${{ inputs.is-valid }} secrets: APPLES: ${{ secrets.APPLES }}
actions/checkoutでpersist-credentials: false
が指定されていないとエラーにするのもいい感じです。
# ❌: persist-credentialsが未指定やtrueだとエラーが出る - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # ⭕: persist-credentialsがfalseだとエラーにならない - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false
つらいポイント
記法に依存した指摘があるのは微妙です。例えば、ジョブが1つならワークフローレベルのpermissions
を指定しても問題ないのですが、ジョブが複数ある場合はワークフローレベルのpermissions
を指定してもエラーになります。これは書き方に依存する5ので、ジョブに一本化したほうがいいんじゃないのかなぁと感じます。
ghalintには現在、pinactやdisable-checkout-persist-credentialsのような自動修正機能はありませんが、これらが追加されると今どきのlintツールらしく使えそうです。良いかどうかは分かりませんが。
アクションのより細かいセキュリティチェックを行う(zizmor)
zizmorはアクションのセキュリティチェックを行うツールで、ghalintよりもチェックが厳し目です。ghalintで検出されないものもzizmorで検出されるので、ghalintで修正した後にzizmorを実行するのがいいでしょう。個人的にはghalintかzizmorのどちらかで十分だと考えているので、zizmorはオプショナルとしました。
Rustで書かれているのですが、各プラットフォーム向けのバイナリインストールは手薄なため、Docker経由で実行しています。
# リポジトリを/githubにマウントしてmedium以上のエラーを検出する docker run -t -v .:/github ghcr.io/woodruffw/zizmor:1.5.2 /github --min-severity medium
例えば次のようなワークフローはzizmorで検出されます。
on: # ❌: pull_request_targetはdangerous-triggersなのでzizmorでエラーになる pull_request_target: branches: ["main"] on: # ⭕: pull_requestはdangerous-triggersじゃないのでzizmorでエラーにならない pull_request: branches: ["main"]
除外ルール
zizmorの除外ルールはコメントベースとコンフィグがあります。コンフィグは行指定なので使いにくく、コメントベースのほうが使いやすいです。
- run: echo "org:${{ matrix.org }} secret:${SECRET}" env: SECRET: ${{ secrets[matrix.secret] }} # zizmor: ignore[overprovisioned-secrets]
嬉しいポイント
ghalintよりも厳しいチェックをしてくれるので、セキュリティ的に安心です。ルールが一覧化されているのでドキュメントが分かりやすいのもいいですね。
つらいポイント
ghalintと重複している項目が多く、ghalintで除外する場合zizmorでも除外する必要があります。ghalintで十分ではという気分になるのは否めません。
zizmorを使っていて、修正がちょっと厄介に感じたものを紹介します。inputsは不正な文字列を差し込まれる可能性があるので環境変数を使えという指摘です。booleanなどinputs次第で問題ない可能性があるにも関わらずinputsに対して一律厳しいのはめんどくさく感じました。
# ❌: runでinputsを直接使おうとするとhigh levelのエラーが出る - name: Output foo input shell: bash run: echo "foo is ${{ inputs.foo != '' && inputs.foo || env.FOO }}" # ⭕: 環境変数にいれてから環境変数をrunで指定する。 - name: Output foo input shell: bash run: echo "foo is ${VALUE}" env: VALUE: ${{ inputs.foo != '' && inputs.foo || env.FOO }}
githubコンテキストも不正な文字列を差し込まれる可能性があるので環境変数を使えという指摘です。ブランチ名やIssueタイトルなど、コミッターが自由にいじれるものはインジェクション余地があるのでまずいのは同意ですが、event_name
などGitHubコンテキストすべてに一律厳しいのはめんどくさく感じました。
# ❌: event_nameを直接使おうとするとhigh levelのエラーが出る - name: file names id: file run: echo "name=${{ github.event_name }}${{ github.event.action != '' && format('_{0}', github.event.action) || ''}}${{ github.event_name == 'push' && format('_{0}', github.ref_type) || ''}}" | tee -a "$GITHUB_OUTPUT" # ⭕: 環境変数にいれてから環境変数をrunで指定する。 - name: file names id: file env: EVENT_NAME: ${{ github.event_name }} EVENT_ACTION: ${{ github.event.action }} REF_TYPE: ${{ github.ref_type }} run: | ACTION_PART="" if [ -n "$EVENT_ACTION" ]; then ACTION_PART="_${EVENT_ACTION}" fi REF_PART="" if [ "$EVENT_NAME" = "push" ]; then REF_PART="_${REF_TYPE}" fi FILENAME="${EVENT_NAME}${ACTION_PART}${REF_PART}" echo "name=$FILENAME" | tee -a "$GITHUB_OUTPUT"
GitHub Actionsで静的解析を自動化する
静的解析ツールをGitHub Actionsで自動実行すると、意図しないミスを素早く検出/修正できます。私はactionlint/ghalint/zizmorをGitHub Actionsで自動実行しています。
GitHub Actionsで同ツールを使うにあたり、aquaでツールバージョンを設定しておくとローカルとCI/CDで同じバージョンを簡単に利用できるのでオススメします。例えば次のようなaqua.yamlをリポジトリルートに設定しておきます。
--- # aqua - Declarative CLI Version Manager # https://aquaproj.github.io/ # checksum: # enabled: true # require_checksum: true # supported_envs: # - all registries: - type: standard ref: v4.332.0 # renovate: depName=aquaproj/aqua-registry packages: - name: rhysd/actionlint@v1.7.7 - name: suzuki-shunsuke/ghalint@v1.2.3
GitHub Actionsワークフローを用意します。zizmorはdockerで実行しています。
name: actionlint on: workflow_dispatch: pull_request: branches: ["main"] paths: - ".github/workflows/**" schedule: - cron: "0 0 * * *" jobs: lint: permissions: contents: read runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: aquaproj/aqua-installer@e2d0136abcf70b7a2f6f505720640750557c4b33 # v3.1.1 with: aqua_version: v2.43.1 # github workflows/action's Static Checker - name: Run actionlint run: actionlint -color -oneline # checkout's persist-credentials: false checker - name: Run ghalint run: ghalint run # A static analysis tool for GitHub Actions - name: Run zizmor run: docker run -t -v .:/github ghcr.io/woodruffw/zizmor:1.5.2 /github --min-severity medium
GitHubユーザー/オーガニゼーションの設定
ワークフローに指定するPATやGITHUB Appは権限を最小限にするして、トークン漏洩時の影響を限定します。漏れたPATで他のリポジトリにも影響したのが今回のCVEのまずいポイントの1つです。
- Legacy PATをやめてFine-grained PATを検討しましょう
- Fine-grained PATはリポジトリや権限を絞りましょう
- GITHUB Appはリポジトリや権限をを絞りましょう
GitHubリポジトリごとの設定
リポジトリの自動アクショントークンのデフォルト権限をwriteからreadに設定して、トークン漏洩時の影響を限定します。漏れたPATでWrite権限があり、アクションでも限定できていなかったのが今回のまずいポイントの1つです。
リポジトリ > Settings > Actions > General > Workflow permissions
でRead
に変更しましょう6
リポジトリのルールセットでタグの更新や削除を禁止することで、PATが漏れて権限があったとしてもアクションの過去リリース上書きを防げます。リリースできる人を限定できるなら、それもある程度有効でしょうが運営に影響するので微妙なラインです。もしコミッターが制限されていて、全員がGPG署名をしているなら、コミット署名の必須化も有効です。GitHub ActionsのコミットはGPG署名されないので、GPG署名を必須にしておくことでGitHub Actionsでのコミットを防げます。
まとめ
いろいろな対応があり、どれを採用するかはチームの考えに依存するでしょう。ただ、少なくとも権限を小さくして、まずい設定になっていないか自動検出だけはしましょう。それだけで、今回のケースは防げましたし、影響を他のリポジトリに伝播させることはありませんでした。
サプライチェイン攻撃といっても、リポジトリ単体で影響を抑えれば影響を波及しないことを再確認したCVEでした。
参考
インシデント報告
- NVD - CVE-2025-30066
- GitHub Actions Supply Chain Attack: A Targeted Attack on Coinbase Expanded to the Widespread tj-actions/changed-files Incident: Threat Assessment (Updated 4/2)
- Semgrep | 🚨 Popular GitHub Action tj-actions/changed-files is compromised
- Harden-Runner detection: tj-actions/changed-files action is compromised - StepSecurity
- tj-actions のインシデントレポートを読んだ
他の人の対応例
ツール一覧
- rhysd/actionlint: :octocat: Static checker for GitHub Actions workflow files | GitHub
- suzuki-shunsuke/ghalint: GitHub Actions linter | GitHub
- woodruffw/zizmor: Static analysis for GitHub Actions | GitHub suzuki-shunsuke/pinact: pinact is a CLI to edit GitHub Workflow and Composite action files and pin versions of Actions and Reusable Workflows. pinact can also update their versions and verify version annotations. | GitHub
- suzuki-shunsuke/disable-checkout-persist-credentials: CLI to disable actions/checkout's persist-credentials | GitHub
- https://gist.githubusercontent.com/nikitastupin/30e525b776c409e03c2d6f328f254965/raw/memdump.py↩
-
新しいリポジトリを作るとデフォルト
read
ですが、以前からのリポジトリはデフォルトwrite
になっています↩ - コミットで同一SHAに衝突されるケースでは問題になる可能性がありますが、それを言い出すとGitという仕組み自体を考え直すことになるので考慮外とします↩
- もしRenovateを使っているならそれでもいいです。↩
- ジョブを増やしたらジョブレベルに書き方直す必要が出てくるので、最初からジョブレベルに書いておくのがいいでしょう↩
-
新しいリポジトリはデフォルト
Read
ですが、以前からのリポジトリはデフォルトWrite
になっています↩