Claude Media
Claude CodeでGitHub Actionsの脆弱性を点検する — zizmorで検出して直す

Claude CodeでGitHub Actionsの脆弱性を点検する — zizmorで検出して直す

pull_request_target、PRタイトルの式展開、権限の広げすぎ、tag指定のaction。GitHub Actionsのworkflowに潜むこの4つを、zizmorで検出してClaude Codeに直させる手順です。

GitHub Actionsのworkflowは、書いた本人が気づかないまま脆弱になります。構文が正しくても、pull_request_target に外部の入力が混ざれば、リポジトリのシークレットに届く経路ができるからです。

zizmorは、この種の問題を静的に見つけるツールです。Claude Codeには、指摘を読んで直し、もう一度検査するところまで任せられます。この記事では、検出する4つの型と、直し方、Claude Codeに回させる設定、CIへの組み込みを順に書きます。

構文の誤りを見つける検査はClaude Codeのactionlint活用にあります。zizmorが見るのは構文ではなく、「この書き方は攻撃に使えるか」です。

zizmorは何を見つけるツールか

zizmorは、GitHub ActionsやDependabot、pre-commitの設定を対象にした、CI/CD向けの静的解析ツールです。リポジトリのREADMEは、見つけるものとして次を挙げています。

  • 攻撃者が制御できる値によるコード実行につながるテンプレートインジェクション
  • 認証情報の意図しない保持と漏えい
  • runnerへの過剰な権限付与
  • 別のコミットを装うなりすまし参照

この記事で扱うのは、そのうち効果の大きい次の4つです。ルール名はzizmorの監査名で、自動修正の欄はルール一覧の表記に沿っています。

ルール見つけるもの自動修正直し方の軸
dangerous-triggers見つけるものpull_request_target などの危険なトリガー自動修正なし直し方の軸トリガーを替える、または検証経路を変える
template-injection見つけるもの${{ }} を run: に直接展開する書き方自動修正unsafeのみ直し方の軸環境変数を経由する
excessive-permissions見つけるもの広すぎる permissions自動修正なし直し方の軸permissions: {} から必要分だけ足す
unpinned-uses見つけるものtagやbranchで指定したaction自動修正unsafeのみ直し方の軸コミットSHAで固定する

「unsafeのみ」は、--fix の既定(safe)では直らず、--fix=all で適用される修正です。dangerous-triggers と excessive-permissions には自動修正がないので、人かClaude Codeが書き換えます。

危険な型: pull_request_targetとPRタイトルの式展開

pull_request_target は、forkからのPRに対して、ベースリポジトリ側のシークレットと権限でworkflowを走らせるトリガーです。zizmorのドキュメントは、これを workflow_run や issue_comment と並ぶ「根本的に危険なトリガー」に数えています。

危険な組み合わせは次のように書けます。PRのタイトルと、PRのheadのチェックアウトが、両方入っています。

# 危険な例(検出される側)
on: pull_request_target
jobs:
  label:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - run: echo "Title is ${{ github.event.pull_request.title }}"

問題は2か所あります。

  1. run: の中の ${{ github.event.pull_request.title }} が、シェルが読む前に文字列として展開されます。タイトルは外部の人が自由に決められる値なので、"; curl ... # のような文字列を入れられると、そのままコマンドになります。
  2. ref: でPR head(外部の人のコード)をチェックアウトしているので、後続のステップがその中身を読み込みます。

zizmorのドキュメントは、「PRのコードを実行しないから安全」という説明を明確に否定しています。引数インジェクション(xargs など)、環境変数インジェクション(LD_PRELOAD)、ローカルファイルの取り込みといった経路で、コードを実行されることがあるという立場です。

このうちテンプレートインジェクションの説明では github.event.issue.title が例に出ています。PRのタイトルも、作成者が自由に決められる点で同じ性質の値です。

Claude Codeにzizmorを回させる

1. インストールして1回実行する

zizmorはHomebrew、PyPI、cargo、Dockerなどで入ります。

brew install zizmor
# または: uv tool install zizmor
zizmor --version
zizmor .github/workflows/

引数にディレクトリを渡すと、その中のworkflowを集めて検査します。workflowだけに絞るなら --collect=workflows を付けます。

指摘は、重大度に応じた終了コードでも返ります。

終了コード意味
0意味指摘なし
11意味最も重い指摘がinformational
12意味最も重い指摘がlow
13意味最も重い指摘がmedium
14意味最も重い指摘がhigh
1 / 2 / 3意味実行中のエラー / 引数の誤り / 検査対象が見つからない

終了コード3はv1.21.0以降の意味です。「0になるまで直す」という繰り返しを、Claude Codeに頼めます。

2. 直す手順をClaude Codeに伝える

検査の結果をそのまま貼って「直して」と頼むだけでも動きます。繰り返し使うなら、CLAUDE.mdに規約として書いておくと、毎回の説明が要りません。

## GitHub Actionsのworkflow
 
- `.github/workflows/` を変更したら、完了前に `zizmor .github/workflows/` を実行する
- 終了コードが0でなければ、指摘のルール名ごとに直し、もう一度実行する
- `pull_request_target` と `workflow_run` は新しく足さない。必要な場合は理由を説明し、確認を取る
- `run:` の中に `${{ github.event.* }}` を直接書かない。`env:` で渡し、シェル変数で参照する
- `permissions: {}` をworkflowの先頭に置き、必要な権限はjobごとに足す
- 指摘を `# zizmor: ignore[ルール名]` で黙らせる前に、理由を報告する

最後の1行が重要です。無視コメントはzizmorの仕様上、行ごとに付けられます。Claude Codeが指摘を消す近道としてこれを使わないよう、先に決めておきます。

3. 自動修正は範囲を決めて使う

zizmorには --fix があります。自動で直せる指摘には、出力に「この指摘は自動修正できる」という注記が付きます。

zizmor --fix=all .github/workflows/ci.yml
git diff

--fix は既定では安全な修正(safe)だけを適用します。上の4つのうち、template-injection と unpinned-uses の修正はunsafe扱いなので、--fix=all か --fix=unsafe-only を指定しないと適用されません。unsafeの修正は人の確認が前提で、ドキュメントも意味の正しさを手で確かめるよう求めています。制約も知っておく必要があります。

  • ファイルをその場で書き換える(リモートの入力には使えない)
  • コメントやインデントを保とうとするが、完全には揃わないことがある
  • unpinned-uses の修正は最新のコミットSHAを取りに行くため、オンラインのGitHubトークンが要る
  • --fix で指摘がすべて直ると、終了コード11以上は使われない

Claude Codeには「--fix=all を実行し、git diff で変更の意味を確かめ、直らなかった指摘は手で直す」と頼みます。差分を読む工程を、手順に含めておく形です。

4つの指摘を直す

template-injection: 環境変数を経由する

直し方は、式をシェル変数に置き換えることです。zizmorのドキュメントの例を、そのまま示します。

# 修正前
- name: Check title
  run: |
    title="${{ github.event.issue.title }}"
    if [[ ! $title =~ ^.*:\ .*$ ]]; then
      echo "Bad issue title"
      exit 1
    fi
 
# 修正後
- name: Check title
  run: |
    title="${ISSUE_TITLE}"
    if [[ ! $title =~ ^.*:\ .*$ ]]; then
      echo "Bad issue title"
      exit 1
    fi
  env:
    ISSUE_TITLE: ${{ github.event.issue.title }}

注意点は2つあります。env: に渡したあと、run: の中で ${{ env.ISSUE_TITLE }} と書いてはいけません。それも式展開なので、危険は残ります。${ISSUE_TITLE} とシェル自身に展開させます。もう1つは、Windowsのrunnerの既定シェルがPowerShellで、${env:ISSUE_TITLE} と書く点です。shell: bash を明示すれば、書き方が1つで済みます。

excessive-permissions: 空から始める

permissions を書かないworkflowは、リポジトリや組織の設定によって広い権限を持つことがあります。ドキュメントの方針は、workflowの先頭を permissions: {} にして、必要な権限をjobごとに足すことです。

name: release
permissions: {}
jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
    steps:
      - run: echo "publish"

権限が必要な場所を絞る狙いです。Claude Codeに頼むときは、「このworkflowの各jobが実際に使う権限を、ステップから読み取って足して」と書きます。必要な権限の見極めには、GitHubSecurityLabの actions-permissions が助けになるとzizmorのドキュメントが紹介しています。

unpinned-uses: コミットSHAで固定する

uses: actions/checkout@v4 のようなtag指定は、上流のリポジトリがtagを付け替えると、中身が変わります。zizmor v1.20.0以降の既定では、公式のactionも含めてすべて、コミットSHAでの固定が求められます。

- uses: actions/checkout@<40桁のコミットSHA> # v6

SHAの横にバージョンをコメントで残すと、あとで人が読めます。固定したあとの更新は、DependabotかRenovateに任せるのが現実的です。固定作業を手伝うツールとして、pinactやfrizbeeなどがドキュメントに挙がっています。zizmor自身の --fix=all も、GitHubトークンがあれば固定できます。

dangerous-triggers: トリガーを替える

直し方は事情で分かれます。ドキュメントにある指針を、状況別に並べます。

状況方針
fork以外のPRしか来ない方針pull_request_target を pull_request に替える
forkからのPRに、コメントなど書き込みが要る方針pull_request_target を残すが、PRのコードを実行しない
workflow_run を使っている方針workflow_call の再利用可能workflowへ作り替える
issue_comment で動かしている方針権限のある人がラベルを付けたときに動かす形にする
どうしても残す方針ブランチ指定と github.repository == ... の確認を付ける

pull_request_target は、PRのベースブランチ側のworkflowファイルを使います。ブランチ指定で対象のブランチを絞れば、古いブランチに残った脆弱なworkflowが使われる余地を減らせます。リポジトリ名の確認は、fork側でActionsを有効にした人を、古い脆弱なコピーから守ります。workflow_run はつねにベースリポジトリで動くため、この確認は効きません。

Claude Code Actionのworkflowを点検する

Claude Codeを呼び出すworkflow自体も、点検の対象です。ここでは、公式のGitHub Actionsドキュメントにある例と、zizmorのルールを突き合わせます。

ドキュメントの @claude 応答用のworkflowは、issue_comment と pull_request_review_comment で起動します。そのため、zizmor v1.31.0以降では dangerous-triggers の対象になります(同バージョンから issue_comment も検査される)。ただし、指摘が出たことと、すぐに悪用できることは別です。

Claude Code Actionは、issueとPRのイベントで、起動した人に書き込み権限があるかを確かめ、無い場合は失敗します。botが起動したときも、allowed_bots に載せたものだけを許します。ドキュメントはこの2つを、実行前の確認として記載しています。指摘を見たら、まずこの確認が効く構成になっているかを調べます。

同じworkflowの actions/checkout@v6 はtag指定なので、既定の設定なら unpinned-uses の対象です。permissions はjobに書かれているため、excessive-permissions の観点では、workflow全体に permissions: {} を置く余地が残ります。

アクション側のセキュリティドキュメントが書いていること

claude-code-actionのリポジトリには、セキュリティの説明があります。点検の判断材料になるのは、次の記述です。

  • pull_request_target と workflow_run は、ベースリポジトリのシークレットで動く。PRのheadをワークスペースの根にチェックアウトしてからactionを呼ぶと、Claudeはその内容を作業ディレクトリとして使う
  • 信頼できないrefを、actionの前にワークスペースの根へチェックアウトしない。ベースのブランチをチェックアウトし、PRの内容が要るなら別のパスに置いて --add-dir で渡す
- uses: actions/checkout@<コミットSHA> # v6
- uses: actions/checkout@<コミットSHA> # v6
  with:
    ref: ${{ github.event.pull_request.head.sha }}
    path: pr-head
- uses: anthropics/claude-code-action@v1
  with:
    claude_args: "--add-dir pr-head"

同じ文書は、PRの上でactionを動かすと、.claude/、.mcp.json、CLAUDE.md などの設定をベースのブランチから復元する、と説明しています。PRが持ち込んだ設定ファイルでClaudeの動作を変えられないようにするためです。一方で package.json やMakefileはPR側のままなので、hookがそれらを経由するコマンドを呼ぶと、PRの内容が効きます。hookのコマンドは自己完結させる、という注意が同じ文書にあります。

さらに、allowed_non_write_users は書き込み権限のない人を通す設定で、文書は「大きなリスク」と明記しています。issues: write だけのラベル付けのような、権限の小さいworkflowに限る使い方です。github_token には ${{ secrets.GITHUB_TOKEN }} を渡し、個人のアクセストークンは使わないよう書かれています。allowed_bots に '*' を使う場合も、permissions を最小にします。

パラメータの全体像はGitHub Actionsのパラメータとclaude_argsの渡し方にあります。

CIで検査を続ける

手元で直しても、次のPRで戻ります。検査をworkflowに入れておくと、戻りが止まります。

name: zizmor
on:
  pull_request:
    paths:
      - ".github/workflows/**"
permissions: {}
jobs:
  zizmor:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@<コミットSHA> # v6
        with:
          persist-credentials: false
      - uses: actions/setup-python@<コミットSHA> # v6
        with:
          python-version: "3.13"
      - run: pip install zizmor
      - run: zizmor --format=github .github/workflows/

ポイントは3つあります。トリガーは pull_request にして、この検査自体が危険なトリガーにならないようにします。persist-credentials: false で、チェックアウトの認証情報が後続に残らないようにします。出力形式の --format=github は、指摘をGitHubの注釈として出します。SARIF形式(--format=sarif)も選べます。この形式のときは、指摘があっても終了コードが0になります。

zizmor の行の pip install はバージョンを指定していません。固定したい場合は zizmor==<バージョン> の形にします。ツール自体の更新で、昨日まで通っていた検査が今日落ちる、という事態を避けられます。

検出が多すぎるときの調整

既定の検査は、指摘の質を重く見た設定です。ドキュメントによると、検査の感度は3つのペルソナで選べます。

ペルソナ用途
regular(既定)用途実用的な指摘に絞る。ローカルでもCIでも向く
pedantic用途コードの臭いも含める
auditor用途誤検出の可能性が高いものも含め、すべて出す

--persona=pedantic や --min-severity=medium のように、フラグで絞り込めます。誤検出や意図的な設計は、行コメントで無視できます。

- run: echo "${{ github.event.issue.title }}" # zizmor: ignore[template-injection]

ファイル全体や特定のルールを外したいときは、設定ファイル zizmor.yml に書きます。unpinned-uses の方針も、設定ファイルで変えられます。"example/*": hash-pin のように、パターンごとにSHA固定を必須にするか、tag指定を許すかを決められます。

つまずきやすい点

症状原因対処
--fix=all で固定できない原因GitHubトークンが無い対処GH_TOKEN を設定して再実行する
リモートのリポジトリに --fix が効かない原因その場書き換えの仕様対処ローカルにcloneして実行する
指摘が消えない原因${{ env.X }} に置き換えた対処${X} とシェル変数に直す
Claude Codeが無視コメントで黙らせた原因CLAUDE.mdに禁止が無い対処無視の前に理由を報告させる
CIで指摘が出ないのに脆弱原因zizmorは静的解析で、実行時の値は見ない対処PRの外から入る値の流れは、人も読む
古いバージョンで新しいルールが出ない原因バージョン差対処zizmor --version を確かめる

zizmorの出力が「問題なし」でも、安全が保証されるわけではありません。ツールは書き方のパターンを見つけます。workflowが何を信頼しているか、という設計の判断は残ります。

まとめ

pull_request_target、式展開、広い権限、tag指定。この4つはどれも、読めば分かる書き方で、読み落とされてきた型です。zizmorに検出を任せ、Claude Codeに「ルール名ごとに直して、もう一度回す」ループを任せると、人は設計の判断に集中できます。

最初の一歩は、既存のworkflowに zizmor .github/workflows/ を1回かけることです。unpinned-uses と excessive-permissions は書き換え方が決まっているので、ここから片付けると進みます。dangerous-triggers は、トリガーを替えられるかを先に考えます。直せない場合は、claude-code-actionのセキュリティ文書にある、ベースのブランチを根にチェックアウトする形が出発点になります。

workflowの作り方そのものはGitHub Actionsのワークフロー作成をClaude Codeで進める手順に、actionの導入はClaude CodeをGitHub Actionsに組み込むにあります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/github-actions
  2. github.com/anthropics/claude-code-action/blob/main/docs/security.md
  3. github.com/zizmorcore/zizmor
  4. docs.zizmor.sh/audits
  5. docs.zizmor.sh/usage
  6. docs.zizmor.sh/quickstart
この記事を共有:XはてブLinkedIn