何が起きるか
CI/CDの信頼境界の不備は、外部の投稿者が作ったコード・文字列・成果物を、秘密情報や書き込み権限のあるジョブが実行してしまう問題です。攻撃者は自分にはない権限を、その自動処理へ行わせます。 ここでは、提出先リポジトリに定義したGitHub ActionsのWorkflowが、テスト後にRunnerという実行環境で解析・配布用のジョブを起動する構成を考えます。
説明用に、外部PRのテスト後に本家の特権Workflowがブランチ名を表示する構成を考えます。名前をBashコードへ直接埋め込むと、fix-$(id)はecho "fix-$(id)"という実行コードになります。Bashは$(id)をコマンド置換として評価し、実行ユーザーを表示するidをジョブ内で動かします。投稿者の端末で動く命令ではありません。
同じ経路で起動した処理がジョブに渡されたトークンを環境変数から読み、攻撃者のサーバーへ送れるなら、攻撃者がその値を取得できます。トークンが書き込みを許していれば、攻撃者はその認証情報でリポジトリを更新できるおそれもあります。実際の被害はジョブへ渡す秘密と権限、ファイルやネットワークの制限に依存します。
正常ならどう動くか
外部PRのコードは、公開用トークンを渡さない環境でテストします。リリース用のジョブは、管理者が承認したコミットと管理された手順で配布物を作ります。テスト側が保存した成果物を後のジョブがダウンロードする場合も、投稿者が成果物のファイル名や内容を操作できることを前提にします。 たとえばテストで作った解析レポートをアーカイブとして保存し、後続の解析ジョブがダウンロードして読みます。このレポートの名前と内容にも外部の投稿者の変更が反映され得るため、データとして取り扱います。
不備と攻撃が成立する流れ
外部PRのテスト成功を契機に、秘密情報を使う後続ワークフローが動く構成を考えます。後続ジョブがPRのブランチ名をrun内のシェルコードへ直接埋め込むと、名前に含まれた記号がシェルの命令として解釈される場合があります。命令が実行される場所は投稿者の端末ではなく、本家のジョブの実行環境です。
成果物からの攻撃では、投稿者が作ったアーカイブを後続ジョブが展開し、後で実行するスクリプトを上書きする場合があります。承認後に変更できるブランチを取得する構成では、承認時とは異なるコードを実行する場合もあります。入力を保存・取得することと、その内容を権限付きで実行してよいことを混同しています。 説明用に、後続ジョブが作業ディレクトリへアーカイブを展開すると、内部のscripts/analyze.shが同じ場所の既存ファイルを上書きする構成を仮定します。投稿者がその内容と内部パスを決められ、後のステップがその場所を起動すれば、秘密付きで投稿者の処理が動きます。承認後のブランチ差し替えでは、投稿者が承認から取得までの間に別コミットをpushすることが変更の契機です。
起こり得る被害と条件
トークン窃取の例では、ジョブで動いた攻撃者のコードが、環境変数から公開用トークンを読み取ります。その値を攻撃者のサーバーへ送信できれば、攻撃者は自分の端末でトークンを入手します。そのトークンをAPIの認証情報として送ることで、トークンが許可する操作を行えるおそれがあります。
改変の例では、攻撃者のコードがジョブの書き込みトークンでリポジトリのファイルを更新したり、公開前の配布パッケージへ不正な処理を追加したりします。書き込みや配布の権限がなければ、その操作まではできません。被害の範囲は、実際に渡された秘密、ジョブの権限、外部接続、ファイルへのアクセス条件で決まります。
防ぎ方と、防げる理由
外部PRを実行する環境には配布用の秘密や書き込み権限を渡さず、配布ジョブは承認済みのコミットIDを取得して実行します。変更できるブランチ名を承認の識別子にしなければ、承認後に先頭コミットを差し替えられても別のコードを実行せずに済みます。
ブランチ名などはrunのシェルコードへ直接埋め込まず、環境変数へ渡し、Bashならecho "$BRANCH_NAME"のように引用して参照します。変数に入れた値の$(...)は自動でコードとして再評価されないため、コード生成による命令の混入を防げます。ただし後からevalなどで再評価しないことが条件です。
外部の成果物は決めた形式のデータとして読み、後で起動するスクリプトの場所へ無条件に展開しません。攻撃者が書き換えたファイルを秘密付きの後続ステップが実行する経路を断つためです。テストの成功だけで作成者や内容を信頼せず、取得するコミットと成果物の出所も検査します。 出所の検査では、許可したリポジトリ・Workflow・承認済みコミットから作られた成果物かを実行IDやコミットIDと照合し、別のPRや別の実行の成果物を取り違えないようにします。それでも外部PRの成果物は、実行するコードではなく検証済みのデータとして扱います。
よくある誤解
テスト成功はコード作成者を信頼できる証明にはなりません。また、秘密を後のステップだけへ渡しても、先に動くコードが、後で実行するスクリプトにトークンの外部送信処理を追加できるなら、秘密を盗まれるおそれがあります。ステップが終わっても同じ実行環境のファイル変更は残り得るためです。仕組みを説明するための例で、ファイルを変更でき、後からそのファイルを実行する構成が前提です。
実装の確認点
pull_request_target、workflow_run、issue_commentで起動する各ジョブについて、誰のコミットを取得するか、誰が作った成果物をどこへ展開するかを確認します。runへ埋め込む式、後で実行するファイル、秘密を渡す時点も調べます。秘密を読む処理だけでなく、その前に誰がファイルを変更できるかまで追います。 runはステップでシェルへ渡すコードを書く場所です。そこへ${{ ... }}で外部値を埋め込むと、Actionsが値へ置き換えた後のコードをシェルが読みます。どのコミットの値がどのコードへ入るかは省略せず確認します。
出典・確認範囲
- [1] Secure use referenceGitHub Docs / 確認日 2026-10-29