
2025/12/08 17:15
GitHub Actions has a package manager, and it might be the worst
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
改良された要約
GitHub Actions には現在ロックファイルが存在しないため、各ワークフロー実行時に可変タグやブランチからすべてのアクションを再解決します。これは
actions/checkout@v4 の変更が後続の実行で使用される SHA を静かに変える可能性があり、再現性を破壊し、攻撃者がビルド間に悪意あるコードを注入できることを意味します。
問題を裏付ける主要な証拠は次の通りです:
- USENIX Security 2022 の研究では、リポジトリの99.7 %が外部アクションを使用し、97 %が未検証作成者からのものであり、18 %がセキュリティ更新を欠いたアクションを実行していることが判明しました。
- 静的汚染分析により、270万件以上のワークフローで4,300件以上のコード注入脆弱性が検出されました。
- コンポジットアクションは内部で依存関係を解決しますが、その依存ツリーを公開しないため、推移的ピンニングは不可能です。
GitHub はイミュータブルリリース、SHA ピンニングポリシー、および作成者検証という限定的な緩和策を提供していますが、これらはトップレベルの依存関係のみを保護します。プラットフォームには中央レジストリやアクション用のセキュリティスキャンもなく、共有可変環境(例:
$PATH の変更)は非決定的な結果につながる可能性があります。
整合性チェックが欠如していることは、OIDC トークンを使用して GitHub Actions でパッケージを公開する下流エコシステムに脅威をもたらします。攻撃されたアクションはより安全なレジストリへと拡散する可能性があります。すべての解決済み SHA を記録したロックファイルは、完全な依存関係グラフを公開し、推移的ピンニングを可能にし、再現可能なビルドを提供します―これが GitHub またはサードパーティツールによるより強力な保護策の採用を促すきっかけとなり得ます。
本文
をまとめた後、GitHub Actions が実際にどのような依存解決アルゴリズムを採用しているのか疑問になりました。ecosyste-ms/package-manager-resolvers
workflow ファイルで
と書くと、依存関係が宣言されます。GitHub はそれを解決し、ダウンロードし、実行します。つまりパッケージ管理です。そこでランナーコードベースに潜り込み、仕組みを調べてみたところ、非常に懸念すべき事実が判明しました。uses: actions/checkout@v4
パッケージマネージャはソフトウェアサプライチェーンセキュリティの中核です
業界は left-pad、event-stream などの事件を受けて数年かけてパッケージマネージャを強化してきました。ロックファイル、整合性ハッシュ、依存関係可視化はオプションではなく基盤です。GitHub Actions はそれらすべてを無視しています。
| 機能 | npm | Cargo | NuGet | Bundler | Go | Actions |
|---|---|---|---|---|---|---|
| ロックファイル | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
| 伝播ピニング | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
| 整合性ハッシュ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
| 依存ツリー可視化 | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
| 解決仕様 | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
核心問題:ロックファイルがない
ほかのパッケージマネージャは何十年も前からこの問題を解決しています。manifest で緩やかな制約を宣言し、リゾルバが特定のバージョンを選択、そしてロックファイルに「これを選んだ」という記録を残します。GitHub Actions は同等の仕組みを持ちません。毎回 workflow ファイルから再解決され、コード自体を変更しなくても結果が変わる可能性があります。
USENIX Security 2022 の研究では 20 万件以上のリポジトリを分析し、以下の事実が明らかになりました。
- 99.7 % が外部で開発されたアクションを実行
- 97 % が検証されていない作成者からのアクションを使用
- 18 % がセキュリティアップデート欠如のアクションを走らせている
研究者は CI/CD システムに必要な四つの基本的安全性プロパティ(受容制御、実行制御、コード制御、シークレットアクセス)を挙げましたが、GitHub Actions はどれも十分に備えていません。静的テイント分析を用いた後続研究では 2.7 百万のワークフロー中 4,300 件以上でコード注入脆弱性が検出され、ほぼすべてのユーザーが第三者コードを検証なしに走らせ、ロックファイルも可視化もない状態で依存関係を持っていることが分かりました。
可変バージョン
actions/checkout@v4 のようにタグを固定しても、そのタグは移動可能です。メンテナは新しいコミットをプッシュし、再タグ付けすることで workflow を静かに変更します。ロックファイルがあれば @v4 が解決した SHA を記録できるので再現性と可読性の両立が可能です。現在は「読みやすいタグ」か「読みづらい SHA」のどちらかを選ばなければなりません。
GitHub は以下の対策を追加しています。
- Immutable releases:公開後にリリース git タグをロック
- 組織ポリシーで SHA ピンニングを強制
- 検証済み作成者からのアクションのみ許可
これらはトップレベル依存に対して有効ですが、伝播依存(トランジティブ)には何も対処しません。
隠れたトランジティブ依存
SHA ピンニングだけでは解決できません。コンポジットアクションは自身の依存を解決しますが、その内容を見たり制御したりする手段がありません。外側の SHA を固定しても、内部で
some-helper@v1 のような可変タグを引っ張ってくると workflow は脆弱です。ロックファイルは全ての解決ツリーを記録し、トランジティブ依存を可視化・ピンニングできます。
JavaScript アクションの研究では 54 % が少なくとも一つのセキュリティ欠陥を含み、多くは間接的な依存から来ています。
tj-actions/changed-files の事件では、侵害されたアクションがトランジティブ依存を更新してシークレットを外部へ流出させました。ロックファイルがあれば、予期しないトランジティブ変更は差分で即座に検知できます。
整合性検証の欠如
npm はロックファイルに整合性ハッシュを記録します。Cargo も
Cargo.lock にチェックサムを書き込みます。インストール時にダウンロード内容が一致するか確認されます。Actions には何もありません。GitHub が SHA の正しいコードを提供してくれることだけを信頼しています。ロックファイルに整合性ハッシュを入れれば、実行中のコードが解決されたものと一致しているか検証できます。
再実行は再現性がない
GitHub のスタッフも明確に言及しています。「workflow があるバージョンのアクションを使用している場合、そのバージョンが強制的にプッシュ/更新されたとき、最新のコードを取得します」。失敗したジョブを再実行すると、元の実行時とは異なるコードが無音で取得されます。キャッシュとの相互作用も問題を悪化させます:キャッシュは成功したジョブにのみ保存されるため、強制プッシュ後の再実行は別のコードでキャッシュを作り直す必要があります。二つの非決定的要因が重なります。ロックファイルがあれば同じロックファイルで同じコードを毎回取得できるので、再実行も deterministic になります。
依存ツリー可視化がない
npm の
npm ls や Cargo の cargo tree のように、完全な依存グラフを確認し、重複やトランジティブ経路を追跡できる機能はありません。Actions では workflow が実際に何に依存しているかを知るために、各コンポジットアクションのソースコードを手動で読む必要があります。ロックファイルがあれば、依存ツリー全体の完全なマニフェストになります。
未文書化の解決セマンティクス
すべてのパッケージマネージャは「どのように依存を解決するか」を明文化しています。npm には仕様、Cargo も仕様があります。Actions の解決方法は未文書化です。ランナーソースコードは公開されており、
ActionManager.cs に全てが詰まっています。以下はその簡略版です。
// Simplified from actions/runner ActionManager.cs async Task PrepareActionsAsync(steps) { // Start fresh every time – no caching DeleteDirectory("_work/_actions"); await PrepareActionsRecursiveAsync(steps, depth: 0); } async Task PrepareActionsRecursiveAsync(actions, depth) { if (depth > 10) throw new Exception("Composite action depth exceeded max depth 10"); foreach (var action in actions) { // Resolution happens on GitHub's server – opaque to us var downloadInfo = await GetDownloadInfoFromGitHub(action.Reference); // Download and extract – no integrity verification var tarball = await Download(downloadInfo.TarballUrl); Extract(tarball, $"_actions/{action.Owner}/{action.Repo}/{downloadInfo.Sha}"); // If composite, recurse into its dependencies var actionYml = Parse($"_actions/{action.Owner}/{action.Repo}/{downloadInfo.Sha}/action.yml"); if (actionYml.Type == "composite") { // These nested actions may use mutable tags – we have no control await PrepareActionsRecursiveAsync(actionYml.Steps, depth + 1); } } }
ここに見られるのは、バージョン制約もなく、重複した同一アクションが二度ダウンロードされること、整合性チェックがない点です。GitHub の API が tarball URL を返し、その SHA に対して正しい内容を返すと信頼するだけ。ロックファイルは「何が解決されたか」の具体的な記録を提供します。
さらに他にある問題点
レジストリがない
アクションは GitHub リポジトリ内に存在し、中央インデックスやセキュリティスキャン、マルウェア検出、タイプスクワッティング防止機能がありません。実際のレジストリなら悪意あるパッケージをフラグ付けしたり、ソースとは別に不変コピーを保管し、セキュリティ対応の一元化を可能にします。Marketplace はリポジトリ検索上層にすぎず、不変メタデータが存在する場所はありません。アクションのソースリポジトリが消失または侵害された場合、フォールバック手段がないのです。
共有可変環境
アクション同士はサンドボックス化されていません。同じ runner 上で複数のアクションが
setup-node を呼び出すと $PATH が変更され、実行順序に依存した結果になります。決定的な解決とは関係ありません。
オフラインサポートなし
Actions は毎回 GitHub から取得します。オフラインインストールモードやベンダリング機構がなく、ネットワーク不可時は CI が停止します。他のパッケージマネージャは依存をベンダー化したりプライベートミラーを設定できます。
名前空間が GitHub ユーザー名
誰でもアカウントを作ればその名前空間を取得でき、アカウント乗っ取りやタイプスクワッティングが可能です。人気アクションのメンテナアカウントが侵害されると、攻撃者は悪意あるコードをプッシュしタグを再付けできます。ロックファイルに整合性ハッシュを入れれば、コード変更を検知できますが、アカウント乗っ取り自体を防ぐことはできません。Go のチェックサムデータベースのような透明ログで「既知良好」ハッシュを追跡する手段も考えられます。
なぜこうなったのか
Actions ランナーは Azure DevOps をフォークしており、社内タスクライブラリが制御された企業向けに設計されていました。GitHub はその基盤にパブリックマーケットプレイスを追加し、信頼モデルを再考せずに拡張しました。コンポジットアクションや再利用可能ワークフローの導入で依存システムが生まれましたが、実装はロックファイル、整合性検証、トランジティブピンニング、依存可視化といったパッケージ管理から学んだ教訓を無視しています。
この問題は CI/CD を超えて影響します。PyPI、npm、RubyGems などのパッケージレジストリでは GitHub Actions で OIDC トークンを使って直接公開できるようになっています。OIDC は「盗まれた資格情報」という攻撃クラスを排除しますが、逆にサプライチェーンセキュリティは完全に GitHub Actions に委ねられる形になります。ワークフローのアクション依存が侵害されれば、より安全なレジストリに悪意あるパッケージが公開される可能性があります。
GitLab CI は 17.9 バージョンで
integrity キーワードを追加し、リモートインクルードの SHA‑256 ハッシュを指定できるようにしました。ハッシュ不一致時はパイプライン失敗と明示的に警告します。GitLab は問題を認識して整合性検証を実装したのです。GitHub は同様の機能要求を閉じました。
Forgejo Actions も GitHub Actions と互換性を保っていますが、倫理的理由で Codeberg に移行するプロジェクトは同じ欠陥 CI アーキテクチャを継承します。Forgejo のメンテナは問題点を認めつつ、互換性維持のために取り残されたままです。GitHub の設計不備は代替サービスにも広がっています。
既存の対策とその限界
- ロックファイルサポート要望(Issue #2195) は 2022 年「未計画」として閉じられました。
- Palo Alto の “Unpinnable Actions” 研究 は SHA ピンニングだけではトランジティブ依存がピンできないことを示しています。
- Dependabot がアクションバージョンを更新するのは有用ですが、根本的な機能は提供しません。
- 自前でベンダリング したり
のようにワークフローをスキャンして脆弱性を検出する手法もありますが、これらは回避策です。zizmor
結論:ロックファイルの必要性
解決済み SHA をすべて記録し(トランジティブも含む)、整合性ハッシュを付与し、依存ツリーを可視化できるロックファイルが不可欠です。GitHub は 3 年前にリクエストを閉じ、再度検討していません。これは単なる「便利」ではなく セキュリティの基盤 に関わる問題です。
参考文献
- Characterizing the Security of GitHub CI Workflows – Koishybayev et al., USENIX Security 2022
- ARGUS: A Framework for Staged Static Taint Analysis of GitHub Workflows and Actions – Muralee et al., USENIX Security 2023
- New GitHub Action supply‑chain attack: reviewdog/action-setup – Wiz Research, 2025
- Unpinnable Actions: How Malicious Code Can Sneak into Your GitHub Actions Workflows
- GitHub Actions Worm: Compromising GitHub Repositories Through the Actions Dependency Tree
- setup‑python: Action can be compromised via mutable dependency