クラウド環境でGitHub ActionsやCIパイプラインを運用し、脆弱性修正のPull Request(変更提案)をレビューしているエンジニアやSREに向けた内容です。テストが通り、スキャナーの警告が消えたのに、実は攻撃経路が別ルートで残っていた、というケースをどう見抜くかを整理します。
2024年8月にWizが公表したSnowflake関連リポジトリのインシデントは、この問題を具体的に示す事例です。GitHub Actionsのワークフローに存在したスクリプトインジェクション(外部入力がスクリプトとして実行されてしまう欠陥)が6月18日に本番へ反映され、Wizは6月23日にそれを悪用できることを確認しています。最終的な修正コミットにはCopilot Autofix(GitHub Copilotが自動生成する修正提案機能)が共著者としてクレジットされていましたが、AIによるレビューはこのインジェクションを検知できていませんでした。
何が起きるか:グリーンなPRが「治った」証拠にならない
セキュリティ修正PRのステータスが緑になる理由は複数あります。ユニットテストが通る、脆弱だったエンドポイントが別のHTTPステータスコードを返す、SASTやDASTなどのスキャナーが元の指摘を再報告しなくなる。いずれも「コードが変わった」ことの証明にはなりますが、「攻撃者が持っていた能力を失った」ことの証明にはなりません。
同じID(認証済みの利用者アカウントや実行主体)が、別のエンドポイント経由で同じ操作に到達できるかもしれません。より広い権限を持つトークンを引き継いだままかもしれません。入力を少し変えるだけで、同等のワークフローが再度トリガーされるかもしれません。テストのグリーンは「症状が消えた」ことを示すだけで、「原因が塞がれた」ことは別の確認が必要です。
影響範囲として深刻なのは、修正PRのレビューをAIアシスト機能が担う場面です。もっともらしい修正の説明文は、挙動としての証拠にはなりません。レビュアーが本当に知る必要があるのは、どのIDが使われたか、どんな前提条件が成立していたか、どの順序でリクエストが飛んだか、修正前のビルドではどこで権限昇格が起きて、修正後のビルドでは具体的にどのステップで拒否されたか、という一連のトレース(実行の軌跡)です。
なぜ起きるか:原因を段階的に分解する
第一の原因は、静的な検知結果と動的な挙動確認を混同していることです。スキャナーが「この行のパターンは危険」と静的に判定するのと、実際にそのパターンを攻撃者視点で実行してみて権限昇格が起きるかを確認するのは、まったく別の検証です。多くのCIパイプラインは前者だけで「修正完了」と判断してしまいます。
第二の原因は、テストの単位が攻撃経路と一致していないことです。ユニットテストは関数やモジュール単位で書かれますが、攻撃はエンドポイントをまたぎ、認可ロジック(誰が何をできるかの判定)を横断して成立します。個別の関数が正しく動いても、複数のコンポーネントの組み合わせで抜け道が残ることがあります。
第三の原因は、修正後の再現テストが「同じ攻撃を同じ手順で再実行」していないことです。多くの現場では、修正前の環境にわざわざ攻撃を再現してから同じ手順を修正後の環境にも当てて比較する、という手間をかけません。ステータスコードの変化やスキャナーの沈黙を代理指標として採用しがちです。
第四の原因は、Copilot AutofixのようなAI支援ツールの説明が「もっともらしい」ことです。修正理由の文章が自然であるほど、レビュアーは安心してしまいます。しかし文章の説得力と、実際に攻撃が止まったかどうかは無関係です。Wiz自身も、コード変更そのものがAI生成だったかは特定できなかったと後に補足しており、教訓はAI支援ワークフロー全体の保証設計にある点が重要です。
自分のプロジェクトが該当するか確認する方法
以下の観点で、自分のリポジトリやパイプラインを見直してみてください。
- GitHub Actionsのワークフローファイル(
.github/workflows/*.yml)で、外部入力(PRタイトル・Issueコメント・ブランチ名など)をrun:ステップに文字列展開でそのまま渡していないか確認する - セキュリティ修正PRのマージ条件が「テストのgreen」だけになっていないか、リポジトリのブランチ保護ルール(Settings > Branches)を確認する
- 脆弱性を修正したPRに、修正前後で同一の攻撃手順を再実行した記録(リクエストログや認可判定ログ)が添付されているか確認する
- 認可テストが単一のロールだけでなく、複数のロール・複数のトークン権限で実行されているか確認する(StackHawkのようなDAST製品はマルチロール認可テストと認証済みDASTをサポートしています)
- Copilot AutofixなどのAI提案をそのままマージするフローがある場合、AIレビューが検知しなかった過去のインシデント(コミット履歴・PRのレビューコメント)が存在しないか棚卸しする
該当する項目があれば、次に挙げる対策の優先度を上げて検討してください。
対策の手順:攻撃の再現をレビューの成果物にする
第一歩は、脆弱性の再現手順をバージョン管理対象の成果物として定義することです。テスト用のID、前提となる状態、順序立てたリクエスト、期待される影響、各ステップで確認すべき証拠を、リポジトリ内のファイルとして記述します。
第二歩は、修正前後のコミットをそれぞれ独立した環境にデプロイすることです。Docker Composeなどでベースラインコミットとパッチ済みコミットを別々に起動し、同じ攻撃トレースを両方に対して再生します。
第三歩は、認可の判定結果・監査イベント・アプリケーションのスパン(OpenTelemetryなどの分散トレーシングにおける処理単位)を両者から収集し、差分を比較することです。理想的な出力は「ベースラインはステップ6で権限を獲得したが、パッチ済みビルドはステップ4で拒否され、以降のトレースに代替の特権呼び出しは現れなかった」という形の、小さな証明になります。
第四歩は、再現が完了しない場合の扱いを明確にすることです。短命トークンの期限切れやサードパーティのコールバック不可などで再生が完結しない場合、チェックは「未解決」と表示すべきで、不確実性を黙ってパス扱いに変換してはいけません。
導入コストが気になる場合は、対象範囲を絞るのが現実的です。まずは認証・オブジェクトレベル認可(特定のリソースへのアクセス権限判定)・チケット管理システム連携のような、認可ロジックが明確なユースケースに限定して始める方法があります。任意の脆弱性を自動生成したり全リポジトリを網羅的にスキャンしたりする必要はなく、既知の再現手順を安全かつ繰り返し可能な形でレビューできるようにする、という範囲に絞ることで運用コストを抑えられます。
まとめ
セキュリティ修正PRのグリーン表示は、コードが変わった証拠であって、攻撃が止まった証拠ではありません。
まず自分のリポジトリのワークフローファイルとブランチ保護ルールを確認し、修正前後で同一攻撃を再現する仕組みがあるかを棚卸ししてください。
なければ、既知の脆弱性1件からでも構わないので、再現手順をリポジトリ内のファイルとして定義し、修正前後の環境で同じトレースを比較する検証を1つ試してみてください。
AI支援ツールの提案理由がもっともらしく見えるときほど、この地道な比較検証が意味を持ちます。