Dev Study
← OWASP Top 10 の群 — 何のリストで、どう作られ、どう読むか
レッスン 9 / 12·約 8 分·第27期 OWASP Top 10 の群

パイプラインは、変更した側のブランチの指示を実行する — CI/CD の PPE

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

同じ「Top 10」でも数えたものが違います。今回は CI/CD の一覧を、何を根拠にまとめたかから読み、その4番目の項目を手元の模型で動かします。

まず結論

CI/CD Top 10 の CICD-SEC-4 Poisoned Pipeline Execution(PPE)は、ソース管理には触れられるがビルド環境には触れられない者が、パイプラインに自分の手順を実行させる危険です。

設定ファイルを保護されたブランチから読んでも、その設定が呼ぶ Makefile やスクリプトが変更側のままなら、変更側の手順が同じ環境で走ります。原文はこれを Indirect PPE と呼びます。

? 読む前に予測

模型のランナーで、設定ファイル ci.sh は保護された想定の main から読み、作業ディレクトリは変更側ブランチから取り出します。変更側は Makefile を書き換えています。実行されたのはどれでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

何を数えた一覧か、そして PPE の主語

番号を読む前に、この一覧が何を根拠にまとめられたかを見ます。index はこう書きます。

The list was compiled on the basis of extensive research and analysis based on the following sources:

続く3項目は、数百の CI/CD 環境の構成の分析(hundreds of CI/CD environments)、専門家との議論、事故や弱点の公表文書です。数えた件数や割合は index 全文のどこにもありません。 並び順の基準も書かれていないので、重要度順とも頻度順とも言えません。著者は同じ1社に所属する2名(CTO と研究部門の責任者)です。

版も独特です。index と各項目の本文は版年を名乗りません。プロジェクトの Roadmap タブにだけ Initial release: v1.0, September 2022 とあります。各項目は CWE 番号も載せていません(確認範囲: CICD-SEC-1〜10 の全文)。

4番目の項目は原文で CICD-SEC-4(ファイル名は 04)です。定義の主語に注目します。

Poisoned Pipeline Execution (PPE) risks refer to the ability of an attacker with access to source control systems - and without access to the build environment, to manipulate the build process ...

ビルド環境に触れられない者が、ビルド環境で手順を走らせる。 走った手順は within the context of the pipeline’s identity、パイプラインの身元で動きます。書き換える対象も、定義の段階で設定ファイルに限っていません(the CI configuration files, or other files which the CI pipeline job relies on)。

同じコースで扱う依存パッケージの入れ替わりとは別の話で、こちらは「パイプラインがどのブランチの指示を実行するか」です。

パイプラインが実行する手順は、どこから来るか
変更側のブランチ設定と、設定が呼ぶファイルを書き換えられる
ランナーが読む
設定ファイルどの ref から読むか
設定が呼ぶファイルMakefile・スクリプト・テスト
同じ身元・同じ変数で実行
パイプラインの実行環境秘密・ノード上の資格・成果物
  • ・設定を書き換えるのが D-PPE、設定が呼ぶファイルに入れるのが I-PPE
  • ・設定だけ守っても、右の箱が変更側なら変更側の手順が走る
具体実物

模型のランナーで、設定を変更側から読む

手元で動かしたのは実在の CI 製品ではなく、20行ほどの自作の模型です。指定した ref の ci.sh を読み、別に指定した ref のソースを取り出した作業ディレクトリで、ダミー変数 DUMMY_SECRET を与えて実行するだけです。実行するコマンドは全部 echo です。

環境は Docker Engine 28.4.0(linux/arm64)上の alpine:3.20(3.20.10)、git 2.45.4、GNU Make 4.4.1、2026-09-28 実行。リポジトリには main(保護されている想定。模型では push を拒む仕組みは置いていません)と、ci.sh と Makefile を書き換えた change-branch があります。

run1.log 28〜55行(抜粋。(c) 対照と (a) 設定を変更側から読む)コマンド出力
### (c) control: config from main, source from main
== runner: config read from ref 'main' / source checked out from ref 'main'
-- config (ci.sh) as read:
| echo "[ci.sh@main] step 1: build via make"
| make build
| echo "[ci.sh@main] step 2: done"
-- Makefile present in the working directory (from 'main'):
| build:
| @echo "[Makefile@main] building the app"
-- executing config:
[ci.sh@main] step 1: build via make
[Makefile@main] building the app
[ci.sh@main] step 2: done
== runner: done
 
### (a) config read from the change branch, source from the change branch
== runner: config read from ref 'change-branch' / source checked out from ref 'change-branch'
-- config (ci.sh) as read:
| echo "[ci.sh@change-branch] this ci.sh was rewritten on the change branch"
| echo "[ci.sh@change-branch] DUMMY_SECRET seen by this step = ${DUMMY_SECRET:-<unset>}"
-- Makefile present in the working directory (from 'change-branch'):
| build:
| @echo "[Makefile@change-branch] this Makefile was rewritten on the change branch"
| @echo "[Makefile@change-branch] DUMMY_SECRET seen by make = $${DUMMY_SECRET:-<unset>}"
-- executing config:
[ci.sh@change-branch] this ci.sh was rewritten on the change branch
[ci.sh@change-branch] DUMMY_SECRET seen by this step = dummy-value-not-a-real-secret
== runner: done

(c) は設定も Makefile も main で、[Makefile@main] building the app が出ます。(a) は設定を変更側から読んだので、main の ci.sh は一切関与せず、変更側の ci.sh がそのまま走り、ダミー変数の値を表示しました。 原文の D-PPE の構造です。2回走らせ、差は時刻とコミットハッシュだけでした。

✓ ここまでの確認

原文の PPE の定義文で、主語になっているのはどんな者ですか。

抽象原理

設定を守ると、残るもの

原文は PPE を3つに分けます(There are three types of PPE:)。

  • D-PPE: 設定ファイルそのものを、保護されていないブランチへの push や PR で書き換える。
  • I-PPE: D-PPE ができない場合でも、設定が参照するファイルに入れる。
  • Public PPE(略して 3PE): 公開リポジトリで、匿名の人の未レビューのコードを CI が走らせる状況。新しい注入先ではなく、D か I を誰ができるかの区分です。

I-PPE の前提は3つ並びます。

  • If the pipeline is configured to pull the CI configuration file from a separate, protected branch in the same repository.

これに別リポジトリに置く場合、CI システムの中で定義する場合が続きます(L31-33)。保護ブランチは3つのうちの1つです。 そのうえで原文は、参照先の例として make、スクリプト、テスト、linter やスキャナの設定の4つを挙げます(L39-42)。

ここで Recommendations の L195 を並べます。

Alternatively, the CI configuration file can be managed in a remote branch, separate from the branch containing the code being built in the pipeline. The remote branch should be configured as protected.

L31 が I-PPE の前提に挙げた構成と、L195 が推奨する構成は同じです。 原文は「保護ブランチでは I-PPE を防げない」とは書いていません。ただ2箇所を並べると、L195 が効くのは文頭のとおり設定ファイルの改ざんに対してで、参照先は別に残る、と読めます。

具体実物

設定は main から、Makefile は変更側から

同じ模型で、設定だけを main から読みます。

run1.log 57〜72行(抜粋。(b) 設定は main、作業ディレクトリは変更側)コマンド出力
### (b) config read from protected main, source from the change branch (Makefile comes from the change branch)
== runner: config read from ref 'main' / source checked out from ref 'change-branch'
-- config (ci.sh) as read:
| echo "[ci.sh@main] step 1: build via make"
| make build
| echo "[ci.sh@main] step 2: done"
-- Makefile present in the working directory (from 'change-branch'):
| build:
| @echo "[Makefile@change-branch] this Makefile was rewritten on the change branch"
| @echo "[Makefile@change-branch] DUMMY_SECRET seen by make = $${DUMMY_SECRET:-<unset>}"
-- executing config:
[ci.sh@main] step 1: build via make
[Makefile@change-branch] this Makefile was rewritten on the change branch
[Makefile@change-branch] DUMMY_SECRET seen by make = dummy-value-not-a-real-secret
[ci.sh@main] step 2: done
== runner: done

読んだ設定は main の3行です。ところが make build は作業ディレクトリの Makefile、つまり変更側の Makefile を読み、その手順が同じ変数のある環境で走りました。

(a) と (b) の差は「秘密が見えたか」ではありません。どちらでもダミー変数は見えました。差は、どのファイルの手順が走ったかです。(a) は ci.sh、(b) は Makefile でした。

見えるものも秘密に限りません。原文の Impact は、秘密のほかにノード上のファイルやクラウドの資格、成果物の差し替え、ネットワーク上の他のホストを挙げます。

✓ ここまでの確認

Recommendations は、設定ファイルを保護された別ブランチで管理する案を挙げています。I-PPE の節と並べたときの関係として正しいのはどれですか。

⇄ 2つを見比べる
具体例 A

D-PPE の構造 — 設定を変更側から読む(模型の (a))

変更側の ci.sh がそのまま実行され、main の ci.sh は関与しませんでした。設定の読み元を保護ブランチに移せば、この経路は塞がります。

具体例 B

I-PPE の構造 — 設定は main、参照先は変更側(模型の (b))

設定は main の3行でしたが、その中の make が変更側の Makefile を読みました。設定の読み元を守っても、この経路は残ります。

2つのログで、変更側の手順が走った入口はそれぞれどのファイルでしたか。

「config (ci.sh) as read」と「Makefile present in the working directory」の from を見比べてください。

この章のまとめ

  • CI/CD Top 10 は数百の環境の分析・専門家との議論・公表文書からまとめた一覧。件数も並び順の基準も書かれていない。本文に版年は無く、Roadmap に v1.0(2022年)。
  • PPE の主語はソース管理に触れられるがビルド環境には触れられない者。手順はパイプラインの身元で走る。
  • 3分類は D-PPE / I-PPE / 3PE。3PE は誰ができるかの区分。
  • I-PPE の前提は3つで、保護ブランチはその1つ。参照先の例は make・スクリプト・テスト・linter などの設定。
  • 模型では、設定を main から読んでも変更側の Makefile の手順が走った。対比の軸は「どのファイルの手順が走ったか」。
  • 原文は「保護ブランチでは防げない」と明言していない。L31 と L195 を並べると、推奨が効く範囲が読める。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

CI/CD Top 10 の index は、一覧を何に基づいてまとめたと書いていますか。

用語集(3語)
PPE(パイプライン実行ポイズニング)
ソース管理に触れられる者が、CI 設定や設定が頼るファイルを書き換え、パイプラインの身元で自分の手順を実行させる危険。CI/CD Top 10 の CICD-SEC-4。
D-PPE / I-PPE / 3PE
設定ファイルそのものを書き換える Direct、設定が参照するファイルに入れる Indirect、それを公開リポジトリで匿名の人ができる Public PPE(略して 3PE)。
ランナー
パイプラインの手順を実際に実行する側。本回で動かしたのは、指定した ref の ci.sh を読んで実行するだけの自作の模型。