GitHub Actions スクリプトインジェクション完全解説 — ${{ }} を run に書いた瞬間、攻撃が始まる
@koki_develop 氏のポストで紹介された Zenn 記事が話題になっています。
書きました。GitHub Actions 触る人は全員知っておいてほしい
【GitHub Actions】スクリプトインジェクションの実践例(koki 氏)は、GitHub Actions ワークフローにおけるスクリプトインジェクションの仕組みを具体的なコード例で解説した記事です。「プライベートリポジトリなら大丈夫?」という疑問にも明確に「安全ではない」と回答しています。
2025 年には GhostAction キャンペーンで 3,325 件のシークレットが窃取され、tj-actions/changed-files のサプライチェーン攻撃では 23,000 以上のリポジトリが影響を受けました。スクリプトインジェクションは理論上の脅威ではなく、現在進行形のリスクです。
スクリプトインジェクションとは何か
GitHub Actions の ${{ }} 式は、シェルがコマンドを解析する前にテンプレートエンジンによって展開されます。この順序が脆弱性の根本原因です。
通常の期待:
${{ github.event.pull_request.title }} → 文字列として処理される
実際の動作:
${{ github.event.pull_request.title }} → 値がそのままシェルスクリプトに埋め込まれる
→ シェルがコマンドとして解釈する
つまり、PR タイトルやブランチ名など攻撃者が制御可能な値が、そのままシェルコマンドの一部になります。
攻撃の実践例
攻撃 1: PR タイトルによるインジェクション
脆弱なワークフロー:
| |
攻撃者が PR タイトルを "; echo INJECTED" に設定すると:
| |
echo "PR title is " と echo INJECTED の 2 つのコマンドが実行されます。INJECTED の部分を任意のコマンドに置き換えれば、シークレットの窃取やコードの改ざんが可能です。
攻撃 2: ブランチ名によるインジェクション
脆弱なワークフロー:
| |
ブランチ名にはスペースを含められませんが、${IFS}(Internal Field Separator)で代用できます。
ブランチ名: main";echo${IFS}INJECTED"
展開後:
| |
攻撃 3: 実際のシークレット窃取
理論的な echo INJECTED ではなく、実際の攻撃では以下のようなペイロードが使われます。
| |
攻撃者が制御可能なコンテキスト変数
GitHub Security Lab が公開している「信頼できない入力」の一覧です。
| コンテキスト | 攻撃ベクトル |
|---|---|
github.event.pull_request.title | PR タイトル |
github.event.pull_request.body | PR 本文 |
github.event.issue.title | Issue タイトル |
github.event.issue.body | Issue 本文 |
github.event.comment.body | コメント本文 |
github.head_ref | PR のソースブランチ名 |
github.event.commits[*].message | コミットメッセージ |
github.event.commits[*].author.name | コミット作者名 |
github.event.commits[*].author.email | コミット作者メール |
github.event.pages[*].page_name | Wiki ページ名 |
これらを run: ステップで直接 ${{ }} に入れた瞬間、インジェクションが可能になります。
防御方法 — 環境変数を経由する
修正は一行の変更です。
脆弱なコード
| |
安全なコード
| |
env: で渡された値はシェル環境変数としてメモリに格納されます。シェルスクリプトの生成時には関与しないため、コマンドインジェクションが成立しません。
脆弱なパターン:
テンプレート展開 → 値がスクリプトに埋め込まれる → シェルが実行
安全なパターン:
テンプレート展開 → 値が環境変数に格納される → シェルがスクリプトを実行
→ 環境変数は文字列として参照
よくある間違い: ${{ env.* }} を使う
| |
${{ env.MY_VAR }} はテンプレート展開されるため、元の値がスクリプトに直接埋め込まれます。必ずシェル変数 ${MY_VAR} を使ってください。
「プライベートリポジトリなら安全?」— いいえ
koki 氏が追記で強調しているポイントです。以下のシナリオでプライベートリポジトリでも攻撃が成立します。
攻撃シナリオ:
1. 侵害された GitHub App が「Contents: Write」+「Pull Requests: Write」権限を持つ
2. GitHub App が悪意のある PR タイトルで PR を作成
3. ワークフローが自動的にトリガーされる
4. スクリプトインジェクションでシークレットが窃取される
「Workflows: Write」権限がなくても、PR の作成だけでワークフローをトリガーできます。プライベートリポジトリに AWS キーを保存するのが安全でないのと同じ理屈です。
2025 年の実際のインシデント
| インシデント | 影響 | 手法 |
|---|---|---|
| GhostAction | 3,325 シークレット窃取(AWS キー・DB 認証情報含む) | ワークフローに悪意あるコードを注入し HTTP POST で外部送信 |
| tj-actions/changed-files(CVE-2025-30066) | 23,000+ リポジトリ | サプライチェーン攻撃でバージョンタグを改ざん。CI ランナーのメモリをダンプ |
| gluestack-ui(CVE-2025-53104) | NPM パッケージの侵害 | GitHub Discussion のタイトル/本文経由でコマンド注入 |
| PyPI トークン窃取 | 多数のリポジトリの PyPI トークン | ワークフロー改ざんで外部サーバーにトークン送信 |
これらは全て、${{ }} 式の不適切な使用やサプライチェーンの信頼関係を悪用した攻撃です。
静的検査ツール — 自動で検出する
actionlint
GitHub Actions ワークフローの静的チェッカーです。${{ }} 式内の信頼できないコンテキスト変数を検出します。
| |
YAML の構文・構造検証に加え、セキュリティパターンも検出します。実行時間は 0.39 秒以内と高速です。
zizmor
GitHub Actions 専用のセキュリティリンターです。インジェクション脆弱性、権限問題、可変タグの使用など、内在的なセキュリティリスクを検出します。
| |
自動修正機能があるのが特徴で、検出した脆弱パターンを安全なパターンに書き換えてくれます。
ツール比較
| ツール | 対象 | 特徴 |
|---|---|---|
| actionlint | 構文 + セキュリティ | 高速。CI に組み込みやすい |
| zizmor | セキュリティ特化 | 自動修正あり |
| CodeQL | コード全般 | GitHub Advanced Security で利用可能 |
| Scorecards | サプライチェーン | スクリプトインジェクション + トークン権限 + アクション固定 |
koki 氏は actionlint と zizmor の併用を推奨しています。それぞれ検出可能な項目が異なり、補完関係にあるためです。
防御のチェックリスト
[ ] run: 内で ${{ }} を直接使っていないか
[ ] env: 経由でシェル変数として渡しているか
[ ] ${{ env.* }} ではなく ${VAR} を使っているか
[ ] サードパーティ Actions はコミットハッシュで固定しているか
[ ] GITHUB_TOKEN の権限は最小限か
[ ] actionlint / zizmor を CI に組み込んでいるか
[ ] pull_request_target トリガーを使っていないか(使う場合は慎重に)
まとめ
${{ }}をrun:に直接書くな: テンプレート展開がシェル解析より先に行われるため、攻撃者が制御可能な値がそのままコマンドになる- 環境変数を経由せよ:
env:でシェル変数に渡すだけで、値は文字列として処理されインジェクションが防止される ${{ env.* }}も危険: テンプレート展開されるため、${VAR}を使うこと- プライベートリポジトリも安全ではない: 侵害された GitHub App や内部脅威により、プライベートリポジトリでもスクリプトインジェクションは成立する
- 2025 年は実害が発生: GhostAction で 3,325 シークレット、tj-actions で 23,000 リポジトリが被害。理論上の脅威ではなく現実のリスク
- actionlint + zizmor で自動検出: CI に組み込んで脆弱パターンを継続的に検出・修正する
- 判断コストをゼロにする: 「この変数は信頼できるか」を毎回考えるのではなく、「常に環境変数を経由する」というルールで統一する
参考
- @koki_develop のポスト
- 【GitHub Actions】スクリプトインジェクションの実践例 — koki(Zenn)
- GitHub Actions を静的検査するツールの紹介 — koki(Zenn)
- Script injections — GitHub Docs
- Keeping your GitHub Actions and workflows secure Part 2: Untrusted input — GitHub Security Lab
- How to secure GitHub Actions workflows: 4 tips — GitHub Blog
- The GhostAction Campaign: 3,325 Secrets Stolen — GitGuardian
- tj-actions/changed-files Supply Chain Attack — Wiz
- 社内用 GitHub Actions のセキュリティガイドライン — メルカリエンジニアリング