tech.guitarrapc.cóm

Technical updates

GitHub ActionsのCVE-2025-30066を受けたワークフローの変化

2025年3月28日にあったtj-actionやreviewdogのセキュリティインシデントCVE-2025-30066を受けて、GitHub Actionsワークフローの書き方が変えてしばらく経ちました。その対応メモを残します。

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が漏洩して意図せず変更される可能性もあるでしょう。私が利用者として改めて認識したポイントは次の通りです。

  1. アクションのタグ/リリースは書き換えられる可能性がある
  2. 利用者のPersonal Access Token (PAT)やGITHUB Appはアクションで漏れる可能性がある
  3. ワークフローやジョブに設定した環境変数/シークレットはアクションを使ってCIログに書き出せる
  4. ワークフローやジョブの権限が高いほどインパクトが大きい
  5. パブリックリポジトリのpull_request_targetイベントの利用はリスクが高い
  6. コミットは別ユーザーで偽れる
  7. 利用できるアクションを限定するのはある程度有効

ポストモーテムで得られる教訓を並べてみましょう。このうち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を無効化する

私は次の流れで自動化を進めました。順に見ていきましょう。

  1. pinactでSHAに変換する
  2. disable-checkout-persist-credentialsでcheckoutアクションのpersist-credentialsを無効化する
  3. Dependabotでアクション更新を自動化
  4. actionlintを実行してワークフロー構文を標準化する
  5. ghalintでセキュリティチェックを実行して、出てきたエラー/警告を修正する
  6. (Optional) zizmorでセキュリティチェックを実行して、出てきたエラー/警告を修正する
  7. GitHub Actionsで静的解析を自動化する

アクションをSHAで固定する(pinact)

pinactはGitHub ActionsのタグやブランチをSHAに一括変換するツールです。SHAに変換することで、今回のインシデントのようにタグに悪意のあるコードが仕込まれた場合でも影響を受けません。3

後述するghalintでSHA指定していないのを検出できるのですが、pinactコマンドを叩くだけで修正できます。

ghalintのエラー例

$ pinact run

嬉しいポイント

コマンドを実行するだけでSHAに変換されるので、手作業より圧倒的に簡単、確実です。SHAにしたいならpinactで99%解決します。コンフィグファイルもありますが、おおむね不要でしょう。

# ❌: tag指定
- uses: actions/checkout@v4

# ⭕: SHA指定
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

ドキュメントもアップデート対象にできますが、これはうまくいかないケースもあるようなのでうまくいったら便利程度でいいでしょう。

つらいポイント

Organization内部で常にブランチを見たいケースでSHAに変換されるのは邪魔です。先ほどの1%に該当するので都度対処でいいです。 設定ファイル.pinact.yamlignore_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認証情報にアクセスできるステップを限定できます。

後述するghalintzizmorでfalseにしていないのを検出できるのですが、disable-checkout-persist-credentialsコマンドを叩くだけで修正できます。

ghalintのエラー例

zizmorのエラー例

$ 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

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

actionlintでruns-onがないのを検出

除外ルール

なかにはルールを除外したい場合もあるでしょう。.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-allwrite-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"

ghalintの検出例

メッセージにエラー原因や修正方法が書かれているので従いましょう。

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_shaaction_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 permissionsReadに変更しましょう6

ワークフロー権限をRead権限に限定する

リポジトリのルールセットでタグの更新や削除を禁止することで、PATが漏れて権限があったとしてもアクションの過去リリース上書きを防げます。リリースできる人を限定できるなら、それもある程度有効でしょうが運営に影響するので微妙なラインです。もしコミッターが制限されていて、全員がGPG署名をしているなら、コミット署名の必須化も有効です。GitHub ActionsのコミットはGPG署名されないので、GPG署名を必須にしておくことでGitHub Actionsでのコミットを防げます。

ルールセットの設定例

まとめ

いろいろな対応があり、どれを採用するかはチームの考えに依存するでしょう。ただ、少なくとも権限を小さくして、まずい設定になっていないか自動検出だけはしましょう。それだけで、今回のケースは防げましたし、影響を他のリポジトリに伝播させることはありませんでした。

サプライチェイン攻撃といっても、リポジトリ単体で影響を抑えれば影響を波及しないことを再確認したCVEでした。

参考

インシデント報告

他の人の対応例

ツール一覧


  1. https://gist.githubusercontent.com/nikitastupin/30e525b776c409e03c2d6f328f254965/raw/memdump.py
  2. 新しいリポジトリを作るとデフォルトreadですが、以前からのリポジトリはデフォルトwriteになっています
  3. コミットで同一SHAに衝突されるケースでは問題になる可能性がありますが、それを言い出すとGitという仕組み自体を考え直すことになるので考慮外とします
  4. もしRenovateを使っているならそれでもいいです。
  5. ジョブを増やしたらジョブレベルに書き方直す必要が出てくるので、最初からジョブレベルに書いておくのがいいでしょう
  6. 新しいリポジトリはデフォルトReadですが、以前からのリポジトリはデフォルトWriteになっています