zizmorでGitHub Actionsを静的解析し、公開ワークフローのサプライチェーンリスクを低減する
はじめに
2026年5月11日、TanStack/Router の npm パッケージ群が侵害され、@tanstack/* の 42 パッケージ・計 84 バージョンに悪意のあるコードが公開されました。ポストモーテムが非常に参考になります。
GitHub Actionsのworkflowファイルも「攻撃可能なコード」として静的に監査しておくべきだと改めて感じる事案でした。
TanStackによる対応・再発防止策の詳細はインシデント直後のfollow-up postに公開されています。
この中で再発防止策の一つとしてAdding zizmor as a required PR check on every repoと挙げられており、Rust製のGitHub Actions静的解析ツールzizmorの存在を知りました。
今回のインシデントを成立させた要因は概ね次の通りです。
pull_request_targetトリガでforkのPRコードをチェックアウトしビルドしてしまう(Pwn Request)- そのジョブが書き出したGitHub Actionsのキャッシュが、後から
mainのrelease workflowにrestoreされる(Cache Poisoning) - restoreされた攻撃コードが、release workflowに付与された
id-token: write権限によりジョブ実行中にランナーのメモリ上で発行されるOIDCトークン(GitHubが発行するJWT)をRunner.Workerプロセスのメモリから読み取り、それをnpm trusted publisher連携経由でregistry.npmjs.orgに直接提示することで悪意のあるバージョンをpublish
なお3つ目について、release workflow内に定義された正規のPublish Packagesステップは実行されておらず、test/cleanupフェーズで動いていたマルウェア(cache restore 経由で混入した依存パッケージの postinstall スクリプトとして起動)が自らid-token: write権限を使ってOIDCトークンを発行させ、registry.npmjs.orgに直接POSTした、という点がポストモーテムで明記されています。
Publish is authenticated via OIDC trusted-publisher binding for TanStack/router release.yml@refs/heads/main — but it does not come from the workflow's defined Publish Packages step, which was skipped because tests failed. It comes from the malware running during the test/cleanup phase, which mints an OIDC token via the workflow's id-token: write permission and POSTs directly to registry.npmjs.org
前述のパターンのうち最初の 2 つには、対応する zizmor の audit ルールが存在します(Pwn Request 系は dangerous-triggers、Cache Poisoning は cache-poisoning)。3 つ目は runtime exploitation のステップなので静的解析の対象外です。
今回はこのzizmorでいくつかのパターンのワークフローがどの程度の指摘を受けるか、その後の修正も含め試してみます。
前提
趣味作っているブログシステムのCI, CDに対して静的解析を行います。
zizmorのバージョンは以下を利用します。
$ zizmor --version
zizmor 1.24.1
zizmorのPersona
zizmor は audit ごとに「どの persona で出すか」が決められており、--persona <name> で出力の粒度を切り替えられる。
| Persona | 用途 | 出る指摘の傾向 |
|---|---|---|
regular(デフォルト) |
一般的な開発者向け | 誤検出が少なく、対処価値が高い指摘のみ。pedantic / auditor 専用の audit は抑制される |
pedantic |
より厳しい lint 的チェック | regular に加えて、慣習・スタイル寄りの指摘(anonymous-definition、concurrency-limits、undocumented-permissions など)が出る |
auditor |
セキュリティ監査者向け | 低 confidence や疑似陽性の可能性があるものも含めて全件出す(pedantic の指摘も含む) |
今回の .github では regular で 30 件中 20 件が抑制されていたが、--persona auditor を付けることで全件確認できた。
各 Persona での実行結果
.github 配下に対して 3 つの persona で実行した時のサマリは以下。各 finding の詳細は、後続の「解析結果: ...」セクションでワークフロー単位の --persona auditor 出力を参照してください。(文字数の都合上)
$ zizmor .github
30 findings (20 suppressed, 8 fixable): 0 informational, 0 low, 5 medium, 5 high
$ zizmor --persona pedantic .github
30 findings (1 suppressed, 8 fixable): 6 informational, 9 low, 6 medium, 8 high
$ zizmor --persona auditor .github
30 findings (0 suppressed, 8 fixable): 6 informational, 9 low, 7 medium, 8 high
auditor は regular / pedantic で抑制される指摘も全て出すため、本記事の以降の解析は --persona auditor で進める。N suppressed は、内部で検出されたが現在の persona ではコンソールに表示されない件数を指す。
出力の見方
はじめに
zizmor の出力は Rust コンパイラの診断フォーマットに似ています。1 件の finding を例に、構成要素を見ていきます。
warning[artipacked]: credential persistence through GitHub Actions artifacts
--> .github/workflows/ci.yaml:13:9
|
13 | - name: Checkout
| _________^
14 | | uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
| |________________________________________________________________________________^ does not set persist-credentials: false
|
= note: audit confidence → Low
= note: this finding has an auto-fix
= help: audit documentation → https://docs.zizmor.sh/audits/#artipacked
severityラベル
warning[artipacked]: credential persistence through GitHub Actions artifacts
行頭の error / warning / help / info が finding の severity を表します。末尾の集計行 ... informational, ... low, ... medium, ... high と対応します。
| ラベル | severity | 主な意味 |
|---|---|---|
error[...] |
high | 確度の高い、優先的に修正すべき問題 |
warning[...] |
medium | 潜在的に問題があり、対応した方が良い |
help[...] |
low | セキュリティ問題というより「ベストプラクティス上やった方が良い」改善提案 |
info[...] |
informational | 補足情報。--pedantic 以上でのみ出るものが多い |
上の例は warning[artipacked] なので severity は medium になります。
audit confidence
= note: audit confidence → Low
severity とは独立した「この検出がどれだけ確度の高い疑いか」を示す軸です。高いほど検出ロジックに迷いが無く、低いほどヒューリスティック寄り(誤検出の可能性が混ざる)になります。
| confidence | 検出の確度 | 解釈 | 対応方針 |
|---|---|---|---|
High |
ほぼ確実 | 静的に判定でき、誤検出の余地がほぼ無い | そのまま信用して対応してよい |
Medium |
状況次第 | 文脈次第で実害が無いケースもある | 影響範囲を確認した上で対応 |
Low |
ヒューリスティック | 検出条件は満たすが実害は文脈依存 | 個別に精査、ケースによっては抑制(ignore:)も視野 |
severity と confidence は独立した軸です。実際の finding でも以下のように組み合わせは様々です。
- 重大度は低いが検出は確実:
info[anonymous-definition](severity = info / confidence = High) - 重大度は高いが誤検出もあり得る:
warning[artipacked](severity = medium / confidence = Low)
ドキュメントリンク
= help: audit documentation → https://docs.zizmor.sh/audits/#artipacked
= の後ろに付くこの help: は、severity ラベルの help[...](= low)とは別物で、Rust の = help: と同じく全 finding 共通で出る参照ドキュメントリンクです。同じ "help" は位置で見分けます。
- 行頭の
help[audit-name]: ...→ severity ラベル(= low) = help: audit documentation → URL→ 参照ドキュメントリンク(severity 問わず常時付く)
auto-fixの有無
= note: this finding has an auto-fix
この note が付いている finding は zizmor --fix で自動修正できます。後述の各表の「Auto-fix」列はこの note の有無に対応します。
解析結果: CI
サマリ
単体実行: zizmor --persona auditor .github/workflows/ci.yaml — 3 findings (1 fixable)
$ zizmor --persona auditor .github/workflows/ci.yaml
INFO zizmor: 🌈 zizmor v1.24.1
INFO audit: zizmor: 🌈 completed .github/workflows/ci.yaml
warning[artipacked]: credential persistence through GitHub Actions artifacts
--> .github/workflows/ci.yaml:13:9
|
13 | - name: Checkout
| _________^
14 | | uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
| |________________________________________________________________________________^ does not set persist-credentials: false
|
= note: audit confidence → Low
= note: this finding has an auto-fix
= help: audit documentation → https://docs.zizmor.sh/audits/#artipacked
info[anonymous-definition]: workflow or action definition without a name
--> .github/workflows/ci.yaml:9:3
|
9 | ci:
| ^^ this job
|
= note: audit confidence → High
= tip: use 'name: ...' to give this job a name
= help: audit documentation → https://docs.zizmor.sh/audits/#anonymous-definition
help[concurrency-limits]: insufficient job-level concurrency limits
--> .github/workflows/ci.yaml:3:1
|
3 | on: [push]
| ^^^^^^^^^^ workflow is missing concurrency setting
...
9 | ci:
| -- job affected by missing workflow concurrency
|
= note: audit confidence → High
= help: audit documentation → https://docs.zizmor.sh/audits/#concurrency-limits
3 findings (1 fixable): 1 informational, 1 low, 1 medium, 0 high
| #[1] | severity | confidence | 該当行 | persona | Auto-fix | 概要 | エラーメッセージの内容 |
|---|---|---|---|---|---|---|---|
| 1 | warning | Low | L13-L14 | regular, pedantic, auditor | あり | actions/checkout で persist-credentials: false を設定していないため、GITHUB_TOKEN が .git/config に残り artifact 経由で漏れうる |
warning[artipacked]: credential persistence through GitHub Actions artifactsdoes not set persist-credentials: false |
| 2 | info | High | L9 | pedantic, auditor | なし | ci ジョブに name: が設定されていない |
info[anonymous-definition]: workflow or action definition without a namethis job |
| 3 | help | High | L3 | pedantic, auditor | なし | workflow に concurrency 設定が無く、同一ブランチへの連続 push で並列実行されてしまう |
help[concurrency-limits]: insufficient job-level concurrency limitsworkflow is missing concurrency setting |
対応(#1): warning[artipacked]
ArtiPACKED 攻撃という手法です。zizmorには、コンソールに出力されている通り、対応するauditのドキュメントがあります。こちらを読んだ上で対応を進めるのが良いでしょう。
ホスティングされたドキュメントのリンクが出力されますが、今回は更新によるリンク切れを防ぐため公開元となるGitHubのマークダウンを利用します。
zizmorのaudit.mdの通り、v6.0.2を使っていることから severity が warning になっています。
zizmor v1.17.0 以降、@actions/checkout の v6.0.0 以上を使用している場合、この監査で見つかる問題の深刻度が低く設定されるようになりました。
これは、@actions/checkout v6.0.0 において、認証情報の保存場所がより誤用を防ぎやすい場所へと変更されたことを反映したものです。
詳細については、GitHub コミュニティのディスカッション(orgs/community?179107)を参照してください。
ArtiPACKED 攻撃詳細はこちらをご覧ください

- ワークフローがコードをチェックアウト(トークンが
.git/configに保存される) - ビルドプロセスが走る
- アーティファクトのアップロードがワークスペース全体(
.,./,${{ github.workspace }}など)を含めてしまう - 攻撃者が GitHub UI からそのアーティファクトをダウンロード
.git/configからGITHUB_TOKENを抽出- ワークフローの権限の範囲でリポジトリにアクセス
本 finding が confidence = Low なのは、zizmor が見ているのは (1) の persist-credentials: false 未設定だけで、実害には (3) の artifact に .git が含まれること、(4) で攻撃者がアクセス可能であることまで揃う必要があり、そこは静的解析では追えないためです。とはいえ前提条件を潰せば後段に依らず安全側に倒せるので対応する価値はあります。
こちらで対応完了とします。
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
- name: Setup node
uses: ./.github/actions/setup-node
対応(#2): info[anonymous-definition]
job定義のname指定漏れ (severity = info)
zizmorの定義は以下
severity が info なので危険性はないが、補足情報を見る限り定義した方が良さそうです。このような情報も検出してくれることで、チームで共通認識や記法を揃える指針になるため非常に便利です。
しかし、name: を省略すると、GitHub ActionsのUI上でそのワークフローやアクションが「匿名」として表示されます。その結果、どの定義が実行されているのかを把握しにくくなります。
!!! note "注意"
この項目はセキュリティ上の影響がないため、--pedantic(厳密モード)オプションを指定した場合のみ実行されるチェック項目です。
nameをつけて対応します。
jobs:
ci:
+ name: CI
runs-on: ubuntu-latest
steps: ...
対応(#3): help[concurrency-limits]
並列実行制限です
zizmorの定義は以下
GitHub Actionsのデフォルト設定では、同じワークフローのインスタンスを複数同時に実行できるようになっています。たとえ新しく開始された実行が古いものを完全に上書きするようなケースであっても、並行して処理が行われます。
これは、特に課金対象となるランナーにおいて、攻撃者にリソースを浪費させる隙を与えることになります。またそれとは別に、ワークフロー実行ID(run ID)ではなく、ワークフロー名やジョブ名などの識別子を使ってアーティファクトを特定しようとする際に、予期せぬ競合状態(レースコンディション)を引き起こす原因にもなり得ます。
こちらも上記通り、severity = help ですが、設定が漏れやすく問題が発覚してから入れるケースもある類の内容です。audit ドキュメントに従い、以下のように concurrency: を追加して解消します。
+ concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
修正後対応
全て適用後再度確認し、指摘事項が見つからないことを確認しました。
$ zizmor --persona auditor .github/workflows/ci.yaml
INFO zizmor: 🌈 zizmor v1.24.1
INFO audit: zizmor: 🌈 completed .github/workflows/ci.yaml
No findings to report. Good job!
すでに解説済みの指摘も含めて最終的な差分は以下となります。
解析結果: デプロイ
サマリ
単体実行: zizmor --persona auditor .github/workflows/deploy.yaml — 2 findings
$ zizmor --persona auditor .github/workflows/deploy.yaml
INFO zizmor: 🌈 zizmor v1.24.1
INFO audit: zizmor: 🌈 completed .github/workflows/deploy.yaml
error[excessive-permissions]: overly broad permissions
--> .github/workflows/deploy.yaml:20:3
|
20 | id-token: write
| ^^^^^^^^^^^^^^^ id-token: write is overly broad at the workflow level
|
= note: audit confidence → High
= help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions
help[undocumented-permissions]: permissions without explanatory comments
--> .github/workflows/deploy.yaml:20:3
|
20 | id-token: write
| ^^^^^^^^^^^^^^^ needs an explanatory comment
|
= note: audit confidence → High
= help: audit documentation → https://docs.zizmor.sh/audits/#undocumented-permissions
2 findings: 0 informational, 1 low, 0 medium, 1 high
| # | severity | confidence | 該当行 | persona | Auto-fix | 概要 | エラーメッセージの内容 |
|---|---|---|---|---|---|---|---|
| 1 | error | High | L20 | pedantic, auditor | なし | workflow レベルで id-token: write を付けており、全ジョブが OIDC トークンを発行できてしまう |
error[excessive-permissions]: overly broad permissionsid-token: write is overly broad at the workflow level |
| 2 | help | High | L20 | pedantic, auditor | なし | permissions: ブロックに何故その権限が必要かの説明コメントが無い |
help[undocumented-permissions]: permissions without explanatory commentsneeds an explanatory comment |
対応(#1): error[excessive-permissions]
deploy.yaml は reusable-deploy.yaml を呼ぶだけですが、reusable workflow の権限は caller 側の許可範囲が上限となるため、caller の id-token: write 自体は必須です。問題は権限の必要性ではなく、それを workflow レベルに置いている点で、現状は同 workflow 内の全ジョブが OIDC トークンを発行できる過剰なスコープになっています。
zizmorの定義は以下
一般的に permissions はできるだけ最小に、利用箇所のできるだけ近くで宣言すべきです。実務的にはほぼ常に workflow レベルで
permissions: {}を設定して既定権限を全て落としておき、必要な job レベルで個別に permissions を付与するのが望ましいです。
caller の workflow レベルは {} に絞り、必要権限は reusable workflow を呼び出す jobs.deploy の job レベルに移します。これで「スコープは最小」「caller→callee の権限継承も維持」を両立できます。なお reusable workflow を呼ぶ場合、callee 側で要求される permissions は caller の job レベル permissions の範囲内に収まっている必要があるため、caller workflow レベルを {} にしただけで callee 側の権限まで落としてしまうと permissions cannot be granted to the called workflow 系のエラーになる点に注意。
- permissions:
- id-token: write
- contents: read
+ permissions: {}
jobs:
deploy:
+ permissions:
+ id-token: write # AWS IAM Role に OIDC AssumeRole するため reusable workflow に継承
+ contents: read
uses: ./.github/workflows/reusable-deploy.yaml
補足: `permissions: {}` 自体を省略するとどうなるか
実際に permissions: {} をコメントアウトして zizmor を回してみると、別の audit がちゃんと検出してくれます。
$ zizmor --persona auditor .github/workflows/deploy.yaml
INFO zizmor: 🌈 zizmor v1.24.1
INFO audit: zizmor: 🌈 completed .github/workflows/deploy.yaml
warning[excessive-permissions]: overly broad permissions
--> .github/workflows/deploy.yaml:1:1
|
1 | / name: Deploy
2 | |
3 | | on:
4 | | push:
... |
36 | | CLOUDINARY_CLOUD_NAME: ${{ secrets.CLOUDINARY_CLOUD_NAME }}
37 | | CLOUDINARY_API_KEY: ${{ secrets.CLOUDINARY_API_KEY }}
| |____________________________________________________________^ default permissions used due to no permissions: block
|
= note: audit confidence → Medium
1 finding: 0 informational, 0 low, 1 medium, 0 high
severity は medium / confidence Medium だが、「workflow に permissions: の意思表明が無い」状態を別のメッセージで拾ってくれる挙動は、デフォルト権限のフォールバックを見逃したくない用途で便利です。
対応(#2): help[undocumented-permissions]
permissions: ブロックに各権限の意図を示すコメントが無いという、--pedantic 限定のコード品質/保守性チェックです。
zizmorの定義は以下
明示的な
permissions:ブロックには各権限の目的をコメントで残すことを推奨します。十分にドキュメント化された permissions は過剰スコープを未然に防ぎ、ワークフローを保守しやすくします。contents: readは自明なため指摘対象外です。
本ファイルでは対応(#1) で workflow レベルの permissions: を {} に絞ったため、workflow レベルの本指摘は同時に解消します。ドキュメントの通り、権限の付与に対して明示的にコメントを残すルールは非常に有用と感じました。
修正後対応
全て適用後再度確認し、指摘事項が見つからないことを確認しました。
$ zizmor --persona auditor .github/workflows/deploy.yaml
INFO zizmor: 🌈 zizmor v1.24.1
INFO audit: zizmor: 🌈 completed .github/workflows/deploy.yaml
No findings to report. Good job!
すでに解説済みの指摘も含めて最終的な差分は以下となります。
解析結果: デプロイのReusable Workflow
サマリ
単体実行: zizmor --persona auditor .github/workflows/reusable-deploy.yaml — 7 findings (2 fixable)
$ zizmor --persona auditor .github/workflows/reusable-deploy.yaml
INFO zizmor: 🌈 zizmor v1.24.1
INFO audit: zizmor: 🌈 completed .github/workflows/reusable-deploy.yaml
warning[artipacked]: credential persistence through GitHub Actions artifacts
--> .github/workflows/reusable-deploy.yaml:28:9
|
28 | - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ does not set persist-credentials: false
|
= note: audit confidence → Low
= note: this finding has an auto-fix
= help: audit documentation → https://docs.zizmor.sh/audits/#artipacked
warning[excessive-permissions]: overly broad permissions
--> .github/workflows/reusable-deploy.yaml:1:1
|
1 | / name: Deploy (Reusable)
2 | |
3 | | on:
4 | | workflow_call:
... |
49 | | CLOUDINARY_API_KEY: ${{ secrets.CLOUDINARY_API_KEY }}
50 | | CLOUDINARY_API_SECRET_KEY_NAME: ${{ vars.CLOUDINARY_API_SECRET_KEY_NAME }}
| |_____________________________________________________________________________________^ default permissions used due to no permissions: block
|
= note: audit confidence → Medium
= help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions
help[template-injection]: code injection via template expansion
--> .github/workflows/reusable-deploy.yaml:43:35
|
43 | ... run: bunx cdk deploy "${{ inputs.stageName == 'prd' && 'p' || 'd' }}-st-main" -c stageName=${{ inputs.stageName }} --require-a...
| --- this run block ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code
|
= note: audit confidence → High
= help: audit documentation → https://docs.zizmor.sh/audits/#template-injection
error[template-injection]: code injection via template expansion
--> .github/workflows/reusable-deploy.yaml:43:104
|
43 | run: bunx cdk deploy "${{ inputs.stageName == 'prd' && 'p' || 'd' }}-st-main" -c stageName=${{ inputs.stageName }} --require...
| --- this run block ^^^^^^^^^^^^^^^^ may expand into attacker-controllable code
|
= note: audit confidence → High
= note: this finding has an auto-fix
= help: audit documentation → https://docs.zizmor.sh/audits/#template-injection
warning[self-hosted-runner]: runs on a self-hosted runner
--> .github/workflows/reusable-deploy.yaml:21:5
|
21 | runs-on: [self-hosted, macOS, ARM64]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ self-hosted runner used here
|
= note: audit confidence → High
= help: audit documentation → https://docs.zizmor.sh/audits/#self-hosted-runner
help[undocumented-permissions]: permissions without explanatory comments
--> .github/workflows/reusable-deploy.yaml:25:7
|
25 | id-token: write
| ^^^^^^^^^^^^^^^ needs an explanatory comment
|
= note: audit confidence → High
= help: audit documentation → https://docs.zizmor.sh/audits/#undocumented-permissions
info[anonymous-definition]: workflow or action definition without a name
--> .github/workflows/reusable-deploy.yaml:20:3
|
20 | deploy:
| ^^^^^^ this job
|
= note: audit confidence → High
= tip: use 'name: ...' to give this job a name
= help: audit documentation → https://docs.zizmor.sh/audits/#anonymous-definition
7 findings (2 fixable): 1 informational, 2 low, 3 medium, 1 high
| # | severity | confidence | 該当行 | persona | Auto-fix | 概要 | エラーメッセージの内容 |
|---|---|---|---|---|---|---|---|
| 1 | error | High | L43 | regular, pedantic, auditor | あり | run: 内で ${{ inputs.stageName }} を直接展開しており、コマンド注入されうる。env: 経由で渡すべき |
error[template-injection]: code injection via template expansionmay expand into attacker-controllable code |
| 2 | warning | Medium | L1-L50 | pedantic, auditor | なし | permissions: ブロック未設定のため、デフォルトの広い permissions が使われる |
warning[excessive-permissions]: overly broad permissionsdefault permissions used due to no permissions: block |
| 3 | warning | High | L21 | auditor | なし | self-hosted runner を使用(GitHub-hosted より攻撃面が広い点に注意) | warning[self-hosted-runner]: runs on a self-hosted runnerself-hosted runner used here |
| 4 | warning | Low | L28 | regular, pedantic, auditor | あり | actions/checkout で persist-credentials: false 未設定 |
warning[artipacked]: credential persistence through GitHub Actions artifactsdoes not set persist-credentials: false |
| 5 | info | High | L20 | pedantic, auditor | なし | deploy ジョブに name: が設定されていない |
info[anonymous-definition]: workflow or action definition without a namethis job |
| 6 | help | High | L43 | pedantic, auditor | なし | run: 内で ${{ inputs.stageName == 'prd' && 'p' || 'd' }} を直接展開しており、入力次第でコマンド注入されうる |
help[template-injection]: code injection via template expansionmay expand into attacker-controllable code |
| 7 | help | High | L25 | pedantic, auditor | なし | id-token: write に説明コメントが無い |
help[undocumented-permissions]: permissions without explanatory commentsneeds an explanatory comment |
対応(#1): error[template-injection]
run: の文字列の中に ${{ inputs.stageName }} を直接展開しています。GitHub Actions のテンプレート展開はシェルや引用符を理解しない単純な文字列置換で、入力次第で任意のシェルコマンドが組み込まれてしまいます。
zizmorの定義は以下
もっとも一般的な template injection は
run:などコード実行ブロック内に現れます。これらのケースでは、インラインのテンプレート展開を、展開結果を値に持つ環境変数で置き換えるのが定石です。これにより脆弱性は回避できます。シェル変数の展開は通常の quoting / expansion ルールに従うためです。なお、${{ env.VARNAME }}は依然テンプレート展開なので、必ず${VARNAME}でシェル側に展開させてください。
stageName を env: 経由でシェル環境変数に渡し、run: 内ではシェル変数として参照します。三項演算 ${{ inputs.stageName == 'prd' && 'p' || 'd' }} も同じ理由で書き換え、シェル側の case で prefix を作る形に直します。
- name: CDK Deploy
working-directory: iac/aws
- run: bunx cdk deploy "${{ inputs.stageName == 'prd' && 'p' || 'd' }}-st-main"
- -c stageName=${{ inputs.stageName }} --require-approval never
+ env:
+ STAGE_NAME: ${{ inputs.stageName }}
+ run: |
+ case "$STAGE_NAME" in
+ prd) PREFIX=p ;;
+ *) PREFIX=d ;;
+ esac
+ bunx cdk deploy "${PREFIX}-st-main" -c "stageName=${STAGE_NAME}" --require-approval never
これにより #6 の help[template-injection](同 audit の三項演算側 col 35 検出)も同時に解消します。
対応(#2): warning[excessive-permissions]
reusable-deploy.yaml には workflow レベルの permissions: 宣言が無いため、デフォルト権限が GITHUB_TOKEN に付与されてしまいます。callee 側にも permissions: {} を置いて、別 caller から呼ばれた場合にもデフォルト権限に依存しない形にします。必要権限は既に jobs.deploy.permissions: で宣言済みなので、追加するのは workflow レベルの {} だけです。
on:
workflow_call:
...
+ permissions: {}
+
jobs:
deploy:
...
permissions:
id-token: write
contents: read
対応(#3): warning[self-hosted-runner]
self-hosted runner は永続ホストのため、GitHub-hosted runner には無い攻撃面があります。
- 過去ジョブの残骸: 前回ジョブが残した認証情報、キャッシュ、依存パッケージなどを後続ジョブが読み取り・改ざんできる
- ネットワーク到達範囲: ホストの設置場所次第で社内ネットワークや IAM ロールに到達できるケースがあり、ジョブを乗っ取られた際の被害範囲が広くなる
zizmorの定義は以下
原則として、セルフホストランナーの使用はプライベートリポジトリのみに留めるべきです。
不特定多数が利用できるパブリックリポジトリにセルフホストランナーを公開することは、例外なくセキュリティリスクを伴います。実際には、ホストのカスタム構成が必要な場合など、パブリックリポジトリでセルフホストランナーを使用せざるを得ないケースも多々あります。そのような場合には、以下の対策を講じることでリスクを最小限に抑えることができます。
外部コントリビューターによるワークフロー実行には手動承認を必須にする
この設定は、リポジトリ、ワークフロー、またはエンタープライズ単位で行うことができます。詳細は GitHubのドキュメント を参照してください。エフェメラル(ジャストインタイム)ランナー のみを使用する
これらのランナーは、一つのジョブを実行するためだけにその都度作成され、終了後すぐに破棄されます。これにより、攻撃者がシステム内に潜伏し続ける(持続性を維持する)ことを困難(不可能ではありませんが)にします。
このうち (1) は GitHub の Settings → Actions → General → "Approval for running fork pull request workflows from contributors" で設定します。選べる 3 段階は次の通り。
| 設定 | 承認が必要なユーザー | 承認なしで実行できるユーザー |
|---|---|---|
| Require approval for first-time contributors who are new to GitHub | GitHub アカウント自体が新しく、かつ本リポジトリへの merge 実績も無いユーザー | 上記以外(GitHub 古参であれば本リポジトリ初コントリビュータでも素通り) |
| Require approval for first-time contributors(デフォルト) | 本リポジトリにまだ commit / PR がマージされていないユーザー | 一度でも PR / commit がマージ済みのユーザー |
| Require approval for all external contributors | リポジトリの member / owner 以外全員 | リポジトリの member / owner のみ |

変更前
self-hosted runner を晒している状況では、過去に PR を 1 回通したアカウント(およびそのアカウントの乗っ取り)経由で runner ホストにジョブを流される余地が残るため、一番厳しい "Require approval for all external contributors"(リポジトリの member / owner 以外は全員承認必須)に変更しました。

変更後
本リポジトリは Apple Silicon ビルド用に macOS ARM64 の self-hosted runner を使用。callee は workflow_call 専用で、caller(deploy.yaml)のトリガを push: [preview, main] / workflow_dispatch に限定しているため、fork PR のコードが self-hosted runner に到達する経路は「PR レビュー → preview / main へのマージ → その後の push トリガ」だけになります。実質的にマージレビューが人間ゲートとして機能している前提のため、runs-on: 直上にコメントで意図を残し、warning は受容します。
jobs:
deploy:
+ # Apple Silicon ビルド用 self-hosted runner。呼び出し元のトリガ制限で pull_request 由来では起動しない。
- runs-on: [self-hosted, macOS, ARM64]
+ runs-on: [self-hosted, macOS, ARM64] # zizmor: ignore[self-hosted-runner]
対応(#7): help[undocumented-permissions]
jobs.deploy.permissions: の id-token: write に説明コメントが無い、という指摘です。zizmor は 同一行のトレイリングコメント を要求するため、permissions: ブロックの直前や id-token: write の上の行にコメントを置いても検出されたままになります。
permissions:
- id-token: write
+ id-token: write # AWS IAM Role に OIDC AssumeRole するため
contents: read
(#4 artipacked / #5 anonymous-definition は他章の対応で説明済みのためスキップ。#6 template-injection は #1 の修正で同時解消。)
修正後対応
全て適用後再度確認し、指摘事項が見つからないことを確認しました。
$ zizmor --persona auditor .github/workflows/reusable-deploy.yaml
INFO zizmor: 🌈 zizmor v1.24.1
INFO audit: zizmor: 🌈 completed .github/workflows/reusable-deploy.yaml
No findings to report. Good job! (1 ignored)
すでに解説済みの指摘も含めて最終的な差分は以下となります。
解析結果: Sphinxドキュメントのデプロイ
サマリ
単体実行: zizmor --persona auditor .github/workflows/docs.yaml — 6 findings (1 fixable)
$ zizmor --persona auditor .github/workflows/docs.yaml
INFO zizmor: 🌈 zizmor v1.24.1
INFO audit: zizmor: 🌈 completed .github/workflows/docs.yaml
warning[artipacked]: credential persistence through GitHub Actions artifacts
--> .github/workflows/docs.yaml:29:9
|
29 | - name: Checkout
| _________^
30 | | uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
31 | | with:
32 | | fetch-depth: 0
| |________________________^ does not set persist-credentials: false
|
= note: audit confidence → Low
= note: this finding has an auto-fix
= help: audit documentation → https://docs.zizmor.sh/audits/#artipacked
error[excessive-permissions]: overly broad permissions
--> .github/workflows/docs.yaml:17:3
|
17 | pages: write
| ^^^^^^^^^^^^ pages: write is overly broad at the workflow level
|
= note: audit confidence → High
= help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions
error[excessive-permissions]: overly broad permissions
--> .github/workflows/docs.yaml:18:3
|
18 | id-token: write
| ^^^^^^^^^^^^^^^ id-token: write is overly broad at the workflow level
|
= note: audit confidence → High
= help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions
help[undocumented-permissions]: permissions without explanatory comments
--> .github/workflows/docs.yaml:17:3
|
17 | pages: write
| ^^^^^^^^^^^^ needs an explanatory comment
18 | id-token: write
| ^^^^^^^^^^^^^^^ needs an explanatory comment
|
= note: audit confidence → High
= help: audit documentation → https://docs.zizmor.sh/audits/#undocumented-permissions
info[anonymous-definition]: workflow or action definition without a name
--> .github/workflows/docs.yaml:25:3
|
25 | build:
| ^^^^^ this job
|
= note: audit confidence → High
= tip: use 'name: ...' to give this job a name
= help: audit documentation → https://docs.zizmor.sh/audits/#anonymous-definition
info[anonymous-definition]: workflow or action definition without a name
--> .github/workflows/docs.yaml:68:3
|
68 | deploy:
| ^^^^^^ this job
|
= note: audit confidence → High
= tip: use 'name: ...' to give this job a name
= help: audit documentation → https://docs.zizmor.sh/audits/#anonymous-definition
6 findings (1 fixable): 2 informational, 1 low, 1 medium, 2 high
こちらはよくある Sphinx のドキュメントをGitHub PagesにデプロイするGitHub Actionsです。
| # | severity | confidence | 該当行 | persona | Auto-fix | 概要 | エラーメッセージの内容 |
|---|---|---|---|---|---|---|---|
| 1 | error | High | L17 | regular, pedantic, auditor | なし | workflow レベルで pages: write を付与しており、全ジョブが Pages を更新できる |
error[excessive-permissions]: overly broad permissionspages: write is overly broad at the workflow level |
| 2 | error | High | L18 | regular, pedantic, auditor | なし | workflow レベルで id-token: write を付与しており、全ジョブが OIDC トークンを発行できる |
error[excessive-permissions]: overly broad permissionsid-token: write is overly broad at the workflow level |
| 3 | warning | Low | L29-L32 | regular, pedantic, auditor | あり | actions/checkout で persist-credentials: false 未設定(fetch-depth: 0 で全履歴取得しているので影響大) |
warning[artipacked]: credential persistence through GitHub Actions artifactsdoes not set persist-credentials: false |
| 4 | info | High | L25 | pedantic, auditor | なし | build ジョブに name: が設定されていない |
info[anonymous-definition]: workflow or action definition without a namethis job |
| 5 | info | High | L68 | pedantic, auditor | なし | deploy ジョブに name: が設定されていない |
info[anonymous-definition]: workflow or action definition without a namethis job |
| 6 | help | High | L17-L18 | pedantic, auditor | なし | pages: write / id-token: write に説明コメントが無い |
help[undocumented-permissions]: permissions without explanatory commentsneeds an explanatory comment |
修正後対応
本ワークフローで検出された 6 件はいずれも CI / デプロイ章で扱った audit type と重複するため、説明と修正は対応する章を参照してください。
| # | audit | 参照先 |
|---|---|---|
| 1, 2 | excessive-permissions | デプロイ: 対応(#1) |
| 3 | artipacked | CI: 対応(#1) |
| 4, 5 | anonymous-definition | CI: 対応(#2) |
| 6 | undocumented-permissions | デプロイ: 対応(#2) |
すでに解説済みの指摘も含めて最終的な差分は以下となります。
解析結果: Renovate契機のCargo.lock更新
サマリ
単体実行: zizmor --persona auditor .github/workflows/renovate-cargo-update.yaml — 6 findings (2 fixable)
こちらは、RenovateでCargo.tomlの更新を契機にCargo.lockを更新するワークフローです。Renovate本体でCargo.lockまで一括更新してくれれば不要になりますが、現状はサポート待ちのためワークフローで補っています。次節の microsoft/apm も同じ理由です。
$ zizmor --persona auditor .github/workflows/renovate-cargo-update.yaml
INFO zizmor: 🌈 zizmor v1.24.1
INFO audit: zizmor: 🌈 completed .github/workflows/renovate-cargo-update.yaml
warning[artipacked]: credential persistence through GitHub Actions artifacts
--> .github/workflows/renovate-cargo-update.yaml:18:9
|
18 | - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
| _________^
19 | | with:
20 | | ref: ${{ github.head_ref }}
| |_____________________________________^ does not set persist-credentials: false
|
= note: audit confidence → Low
= note: this finding has an auto-fix
= help: audit documentation → https://docs.zizmor.sh/audits/#artipacked
error[excessive-permissions]: overly broad permissions
--> .github/workflows/renovate-cargo-update.yaml:10:3
|
10 | contents: write
| ^^^^^^^^^^^^^^^ contents: write is overly broad at the workflow level
|
= note: audit confidence → High
= help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions
help[undocumented-permissions]: permissions without explanatory comments
--> .github/workflows/renovate-cargo-update.yaml:10:3
|
10 | contents: write
| ^^^^^^^^^^^^^^^ needs an explanatory comment
|
= note: audit confidence → High
= help: audit documentation → https://docs.zizmor.sh/audits/#undocumented-permissions
error[bot-conditions]: spoofable bot actor check
--> .github/workflows/renovate-cargo-update.yaml:14:9
|
13 | update-and-ci:
| ------------- this job
14 | if: github.actor == 'renovate[bot]'
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ actor context may be spoofable
|
= note: audit confidence → High
= note: this finding has an auto-fix
= help: audit documentation → https://docs.zizmor.sh/audits/#bot-conditions
info[anonymous-definition]: workflow or action definition without a name
--> .github/workflows/renovate-cargo-update.yaml:13:3
|
13 | update-and-ci:
| ^^^^^^^^^^^^^ this job
|
= note: audit confidence → High
= tip: use 'name: ...' to give this job a name
= help: audit documentation → https://docs.zizmor.sh/audits/#anonymous-definition
help[concurrency-limits]: insufficient job-level concurrency limits
--> .github/workflows/renovate-cargo-update.yaml:3:1
|
3 | / on:
4 | | pull_request:
5 | | paths:
6 | | - 'Cargo.toml'
7 | | - 'apps/blog-api/**/Cargo.toml'
| |_____________________________________^ workflow is missing concurrency setting
...
13 | update-and-ci:
| ------------- job affected by missing workflow concurrency
|
= note: audit confidence → High
= help: audit documentation → https://docs.zizmor.sh/audits/#concurrency-limits
6 findings (2 fixable): 1 informational, 2 low, 1 medium, 2 high
| # | severity | confidence | 該当行 | persona | Auto-fix | 概要 | エラーメッセージの内容 |
|---|---|---|---|---|---|---|---|
| 1 | error | High | L10 | pedantic, auditor | なし | workflow レベルで contents: write を付与しており全ジョブが書き換え可能 |
error[excessive-permissions]: overly broad permissionscontents: write is overly broad at the workflow level |
| 2 | error | High | L14 | regular, pedantic, auditor | あり | github.actor == 'renovate[bot]' は spoof 可能 |
error[bot-conditions]: spoofable bot actor checkactor context may be spoofable |
| 3 | warning | Low | L18-L20 | regular, pedantic, auditor | あり | actions/checkout で persist-credentials: false 未設定(PR head checkout) |
warning[artipacked]: credential persistence through GitHub Actions artifactsdoes not set persist-credentials: false |
| 4 | info | High | L13 | pedantic, auditor | なし | update-and-ci ジョブに name: が設定されていない |
info[anonymous-definition]: workflow or action definition without a namethis job |
| 5 | help | High | L10 | pedantic, auditor | なし | contents: write に説明コメントが無い |
help[undocumented-permissions]: permissions without explanatory commentsneeds an explanatory comment |
| 6 | help | High | L3-L7 | pedantic, auditor | なし | workflow に concurrency 設定が無い |
help[concurrency-limits]: insufficient job-level concurrency limitsworkflow is missing concurrency setting |
対応(#2): error[bot-conditions]
zizmorの定義は以下
一般的に、
github.actorを使って実行者の正当性を確認するだけでは不十分です。ほとんどの場合、github.event.pull_request.user.loginなどを使用すべきです。なぜなら、このコンテキストはプルリクエストを最後に変更した人ではなく、プルリクエストを「作成した」本人を指すからです。
本ワークフローは if: github.actor == 'renovate[bot]' で「Renovate Bot による実行か」を判定していますが、github.actor はプルリクエストを最後に変更した人を指すだけで、プルリクエストを作成した本人ではありません。攻撃者は HEAD コミットだけ renovate[bot] 名義で push し、その先祖に攻撃コードを混ぜ込むことで github.actor によるチェックをすり抜けられます。on: pull_request トリガなので、プルリクエストを作成した本人を指す github.event.pull_request.user.login で置き換えます。
なお renovate[bot] という login は GitHub App としての Renovate(https://github.com/apps/renovate)をリポジトリにインストールしている場合の名前です。Self-hosted Renovate や PAT 経由で動かしている場合は別の login になるため、gh pr view <PR> --json author などで実際の author を確認してから値を合わせてください。
jobs:
update-and-ci:
- if: github.actor == 'renovate[bot]'
+ if: github.event.pull_request.user.login == 'renovate[bot]'
修正後対応
本セクションで扱った #2 以外の 5 件は他章の対応で説明済みのため、参照先は以下の通り。
| # | audit | 参照先 |
|---|---|---|
| 1 | excessive-permissions | デプロイ: 対応(#1) |
| 3 | artipacked | CI: 対応(#1) |
| 4 | anonymous-definition | CI: 対応(#2) |
| 5 | undocumented-permissions | デプロイ: 対応(#2) |
| 6 | concurrency-limits | CI: 対応(#3) |
全て適用後再度確認し、指摘事項が見つからないことを確認しました。
$ zizmor --persona auditor .github/workflows/renovate-cargo-update.yaml
INFO zizmor: 🌈 zizmor v1.24.1
INFO audit: zizmor: 🌈 completed .github/workflows/renovate-cargo-update.yaml
No findings to report. Good job!
すでに解説済みの指摘も含めて最終的な差分は以下となります。
解析結果: Renovate契機のmicrosoft/apmのlock更新
サマリ
単体実行: zizmor --persona auditor .github/workflows/renovate-apm-update.yaml — 6 findings (2 fixable)
$ zizmor --persona auditor .github/workflows/renovate-apm-update.yaml
INFO zizmor: 🌈 zizmor v1.24.1
INFO audit: zizmor: 🌈 completed .github/workflows/renovate-apm-update.yaml
warning[artipacked]: credential persistence through GitHub Actions artifacts
--> .github/workflows/renovate-apm-update.yaml:20:9
|
20 | - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
| _________^
21 | | with:
22 | | ref: ${{ github.head_ref }}
| |_____________________________________^ does not set persist-credentials: false
|
= note: audit confidence → Low
= note: this finding has an auto-fix
= help: audit documentation → https://docs.zizmor.sh/audits/#artipacked
error[excessive-permissions]: overly broad permissions
--> .github/workflows/renovate-apm-update.yaml:9:3
|
9 | contents: write
| ^^^^^^^^^^^^^^^ contents: write is overly broad at the workflow level
|
= note: audit confidence → High
= help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions
help[undocumented-permissions]: permissions without explanatory comments
--> .github/workflows/renovate-apm-update.yaml:9:3
|
9 | contents: write
| ^^^^^^^^^^^^^^^ needs an explanatory comment
|
= note: audit confidence → High
= help: audit documentation → https://docs.zizmor.sh/audits/#undocumented-permissions
error[bot-conditions]: spoofable bot actor check
--> .github/workflows/renovate-apm-update.yaml:13:9
|
12 | update-and-audit:
| ---------------- this job
13 | if: github.actor == 'renovate[bot]'
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ actor context may be spoofable
|
= note: audit confidence → High
= note: this finding has an auto-fix
= help: audit documentation → https://docs.zizmor.sh/audits/#bot-conditions
info[anonymous-definition]: workflow or action definition without a name
--> .github/workflows/renovate-apm-update.yaml:12:3
|
12 | update-and-audit:
| ^^^^^^^^^^^^^^^^ this job
|
= note: audit confidence → High
= tip: use 'name: ...' to give this job a name
= help: audit documentation → https://docs.zizmor.sh/audits/#anonymous-definition
help[concurrency-limits]: insufficient job-level concurrency limits
--> .github/workflows/renovate-apm-update.yaml:3:1
|
3 | / on:
4 | | pull_request:
5 | | paths:
6 | | - 'apm.yml'
| |_________________^ workflow is missing concurrency setting
...
12 | update-and-audit:
| ---------------- job affected by missing workflow concurrency
|
= note: audit confidence → High
= help: audit documentation → https://docs.zizmor.sh/audits/#concurrency-limits
6 findings (2 fixable): 1 informational, 2 low, 1 medium, 2 high
| # | severity | confidence | 該当行 | persona | Auto-fix | 概要 | エラーメッセージの内容 |
|---|---|---|---|---|---|---|---|
| 1 | error | High | L9 | pedantic, auditor | なし | workflow レベルで contents: write を付与しており、全ジョブがリポジトリ書き換え可能 |
error[excessive-permissions]: overly broad permissionscontents: write is overly broad at the workflow level |
| 2 | error | High | L13 | regular, pedantic, auditor | あり | github.actor == 'renovate[bot]' は spoof 可能(実行者は別人でも actor を装える)。github.event.sender 等を使うべき |
error[bot-conditions]: spoofable bot actor checkactor context may be spoofable |
| 3 | warning | Low | L20-L22 | regular, pedantic, auditor | あり | actions/checkout で persist-credentials: false 未設定(PR head を checkout しているので特に注意) |
warning[artipacked]: credential persistence through GitHub Actions artifactsdoes not set persist-credentials: false |
| 4 | info | High | L12 | pedantic, auditor | なし | update-and-audit ジョブに name: が設定されていない |
info[anonymous-definition]: workflow or action definition without a namethis job |
| 5 | help | High | L9 | pedantic, auditor | なし | contents: write に説明コメントが無い |
help[undocumented-permissions]: permissions without explanatory commentsneeds an explanatory comment |
| 6 | help | High | L3-L6 | pedantic, auditor | なし | workflow に concurrency 設定が無い |
help[concurrency-limits]: insufficient job-level concurrency limitsworkflow is missing concurrency setting |
修正後対応
本ワークフローで検出された 6 件はいずれも CI / デプロイ / Renovate契機のCargo.lock更新 章で扱った audit type と重複するため、説明と修正は対応する章を参照してください。
| # | audit | 参照先 |
|---|---|---|
| 1 | excessive-permissions | デプロイ: 対応(#1) |
| 2 | bot-conditions | Renovate(Cargo): 対応(#2) |
| 3 | artipacked | CI: 対応(#1) |
| 4 | anonymous-definition | CI: 対応(#2) |
| 5 | undocumented-permissions | デプロイ: 対応(#2) |
| 6 | concurrency-limits | CI: 対応(#3) |
すでに解説済みの指摘も含めて最終的な差分は以下となります。
さいごに
全 6 ワークフローを --persona auditor で通し、self-hosted runner の意図的な ignore 1 件を除いて対応しました。
confidence の指標のおかげで、偽陽性かどうかの判断が付きやすく便利です。auditor まで上げても明らかに不要な対応は全く無い印象で、ノイズに埋もれず全件向き合う価値があるツールだと感じました。
ファイルごとに表を分割した。各表の中は severity(
error>warning>info>help)順。「persona」列はその指摘を検出する persona のリスト。デフォルト persona はregularなので、列にregularが含まれていればオプションなしのzizmor .githubでも検出される。 ↩︎







