コドモン Product Team Blog

株式会社コドモンの開発チームで運営しているブログです。エンジニアやPdMメンバーが、プロダクトや技術やチームについて発信します!

できることから始めるPHPプロジェクトのOSSサプライチェーン攻撃対策

春のブログリレー、市川さんからバトンを受け取りました、プロダクト開発部の塚原です。

本日4月27日は、哲学の日らしいです。そこで、ソクラテスの言葉を借りて、日々のデプロイに怯える(?)私たちへのアドバイスを。

「ぜひパッケージ管理しなさい。安全なパッケージに恵まれれば、運用は幸せになる。脆弱なパッケージに出会ってしまえば……私のように、セキュリティ記事を書く機会を得られるのだから。」

。。。残念ながら哲学の域には届いていませんが、今日はOSSサプライチェーン攻撃対策とComposerに関するお話をします。

目次


なぜ今、OSSサプライチェーンセキュリティなのか

2026年3月、OSSエコシステムを標的にしたサプライチェーン攻撃が立て続けに発生しました。

  • 3月19日:セキュリティスキャナ Trivy の GitHub Actions が侵害(GHSA-69fq-xp46-6x23)。CI 実行だけで SSH 鍵やクラウドトークンが窃取される状態に
  • 3月24日:Trivy の侵害で流出したクレデンシャルを起点に、PyPI の LiteLLM へ連鎖(GHSA-5mg7-485q-xm76)。数時間にわたり悪意あるバージョンが配布
  • 3月31日:npm の axios(週間約1億ダウンロード)のメンテナアカウントが乗っ取られ、RAT(遠隔操作ツール)を仕込んだバージョンが公開(GHSA-fw8c-xr5c-95f9

自分たちがコードをどれだけ丁寧に書いても、依存しているOSSが侵害されれば、その瞬間から攻撃の入り口になります。JavaScriptエコシステムで長年信頼されてきた axios でさえ例外ではありませんでした。これが今のOSSサプライチェーン攻撃の怖さです。

私たちのメインプロダクトは PHPで構築されており、日々の開発で Composer を使っています。この出来事をきっかけに「Composer はどこまで守れるのか、私たちはどこまで対応できているのか」を改めて整理しました。

本記事では、セキュリティ全体の取り組みのうち「パッケージ管理」の層にフォーカスして、Composer は今どこまで対応できているかと私たちの実装をご紹介します。インフラ側の WAF や定期的な脆弱性診断など、他の防衛ラインと組み合わせた多層防御の一部として、アプリケーション層での取り組みを紹介します。


OSSサプライチェーン攻撃の対策構造

攻撃の詳細については、PHPer として同じ問いを立てた o0h さんの記事がとてもよく整理されています。私自身、こちらの記事から多くの気づきをいただきました。この記事を読むうえでも参考になると思います。

daisuki.nichiyoubi.land

この記事の整理をお借りすると、サプライチェーン攻撃への対策は大きく 3 つの考え方に分解できます。

考え方 内容
Transparency 変更を検証・追跡可能にする Packagist Transparency Log
Filter 既知の悪意あるパッケージをブロックする Aikido Security 連携(Composer 2.10〜)
CoolDown 最新バージョンの採用を一定期間見送る pnpm minimumReleaseAge、npm min-release-age

これに加えて「脆弱性の検知と修正(CVE 対応)」があります。攻撃者によるアカウント乗っ取りのような悪意ある侵害とは性質が異なりますが、後述する私たちの実装はこちらの対策も含んでいます。


Composer は今どこまで対応できているか

Composer がセキュリティ面でどう進化してきたかを、バージョン軸で整理します。

バージョン 機能
2.2 allowedPlugins でプラグインを許可リスト管理できるように
2.4 composer audit コマンドが追加(CVE 検知)
2.7 audit が abandoned パッケージ検出時にも non-zero で終了(CI フレンドリーに)
2.9 require / update 時に既知脆弱パッケージを自動ブロック。Packagist に Transparency Log 導入
2.10-RC1 Aikido Security 連携による悪意あるパッケージの Filter 機能
2.11(予定) CoolDown(minimum-release-age)機能

2.9 で「透明性(Transparency)」、2.10 で「フィルタリング(Filter)」と、前述の 3 つの対策のうち 2 つが着実に実装されてきました。残る「遅延(CoolDown)」は 2.11 に向けて実装が進んでいます。

ただし composer install はデフォルトではブロックしない

私たちが運用の中で『ここが落とし穴だな』と感じたポイントがあります。audit.block-insecure の公式ドキュメントに明記されている通り、2.9 で導入された自動ブロックが有効になるのは require / update / delete の実行時です。

composer.lock が存在する環境で composer install を実行した場合、つまり CI/CD でのデプロイ時や開発環境の初期構築時は、脆弱なパッケージのブロックがかかりません。lock ファイルに記録されたバージョンをそのままインストールするためです。

なお Composer 2.9 以降、composer install --audit というフラグも用意されており、インストールと同時に脆弱性チェックを実行できます。ただしこのフラグは付け忘れるリスクがあります。私たちがどう対処しているかは後述します。


Composer の CoolDown が「まだない」理由

CoolDown に相当する機能は Composer にはまだありません。これは単に実装が追いついていないだけでなく、技術的に解決が難しい問題を抱えています。

GitHub の Issue を追うと、その経緯が見えてきます。

  • Issue #12552(2025年9月):「pnpm と同様の機能を追加してほしい」という提案が「Controversial / Low-Prio」としてクローズ。当時のメンテナの判断は「CoolDown は Dependabot や Renovate 側でやればよく、パッケージマネージャ本体がやることではない」というものでした
  • Issue #12633(2025年11月):改めて提案が上がると、今度はメンテナ Seldaek 自身が態度を変えます。きっかけは "We should all be using dependency cooldowns" という記事で、Seldaek は「数日前にこの記事を読んで、何かできるかもしれないと考えが変わった」とコメント。自らマイルストーンを設定し実装へ向けて動き出しました
  • PR #12692(2025年12月〜):コミュニティメンバーによる実装 PR が作成され、Seldaek もレビューに参加。ただし実装中に根本的な問題が発覚しました。メンテナのコメントを引用します(筆者訳)。

リリースはパッケージ作成者の管理下にあるため、攻撃者は簡単にリリース日を偽装して過去に設定できてしまう。そのため publish_date のような、パッケージリポジトリのみが所有し、パッケージ側では設定できない新しい日付フィールドが必要だ。

この課題を理由にマイルストーンが 2.10 から 2.11 へスライド。現在も PR はオープンのままレビューが続いています(2026年4月時点)。

composer.json や Git タグに記載された日付は改ざん可能であり、Packagist がサーバーサイドで付与する「登録日時」のようなフィールドをメタデータに加えるまで、改ざん耐性のある CoolDown は実現が難しい状態です。

また Packagist 側でも、サプライチェーンセキュリティ強化の取り組みとしてTransparency Log の開発が進んでいます。これは CoolDown の機能ではありませんが、バージョンのリリースや削除をサーバーサイドで記録するこの仕組みが整えば、CoolDown が必要とする「改ざん不能な登録日時」の問題も解決に近づきます。

blog.packagist.com

Composer も着実に追いついてきています。一度は「不要」と判断された機能が、コミュニティの声と現実のインシデントを受けて実装へ動き出しました。そのような誠実な意思決定のプロセスを間近で見られるのも、オープンソースの醍醐味だと感じています。PHP エコシステムのユーザーとして、Packagist と Composer コミュニティの誠実な取り組みをこれからも応援していきたいと思います。


コドモンで取り組んでいること

完璧なセキュリティ対策は存在しません。Composer でできることには限界があり、それを理解した上で、現時点で取れる手段を地道に積み重ねていくことが現実的な向き合い方だと考えています。

ここでは「Composer の機能を活かすこと」「Composer のギャップを CI と開発フローで補完すること」の 2 軸で積み重ねた実装を紹介します。

1. CI でのcomposer audit (PR ゲート)

composer.lock が変更された PR に対して composer audit --locked --no-dev を実行しています。脆弱なバージョンへの更新を develop ブランチへのマージ前にブロックするのが目的です。

# .github/workflows/composer-audit.yml(抜粋)
name: PR PHP Composer Audit

on:
  pull_request:
    branches:
      - develop
  workflow_dispatch:

jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      composer_dirs: ${{ steps.dirs.outputs.composer_dirs }}
    steps:
      - uses: actions/checkout@...
      - uses: dorny/paths-filter@...
        id: filter
        with:
          list-files: json
          filters: |
            composer:
              - '**/composer.lock'
      - name: Extract directories
        id: dirs
        run: |
          dirs=$(echo '${{ steps.filter.outputs.composer_files }}' \
            | jq -c '[.[] | split("/")[:-1] | join("/")]')
          echo "composer_dirs=$dirs" >> "$GITHUB_OUTPUT"

  run-composer-audit:
    needs: changes
    if: needs.changes.outputs.composer_dirs != '[]'
    runs-on: ubuntu-latest
    strategy:
      matrix:
        dir: ${{ fromJson(needs.changes.outputs.composer_dirs) }}
    steps:
      - uses: actions/checkout@...
      - name: Run Composer Audit
        run: composer audit --locked --no-dev --ignore-severity=low --ignore-severity=medium
        working-directory: ${{ matrix.dir }}

私たちが意識した設定ポイントは 4 つです。

--locked オプションcomposer.lock に記録されたパッケージを対象に監査します。lock ファイルがない状態では composer.json のバージョン範囲に基づく解決結果が対象になりますが、実際にインストールされるものと一致しない可能性があるため、--locked を明示しています。

--no-dev オプション:本番環境に含まれない開発用依存関係を監査対象から除外し、CIの検知ノイズを抑えています。

--ignore-severity オプション:low と medium の脆弱性は CI でブロックせず、high と critical のみを対象にしています。すべての severity でブロックすると対応コストが高くなりすぎるため、重篤度に応じて運用を分けています。

composer.lock 変更時のみ実行dorny/paths-filtercomposer.lock の変更を検知し、変更があったディレクトリのみを対象に実行します。複数プロジェクトが混在するモノレポ構成でも、変更のあったプロジェクトだけを効率的にチェックできます。

この仕組みで、composer.lock に脆弱なバージョンが含まれていた場合はマージを止めます。なお composer install はデフォルトでは脆弱なパッケージをブロックしません(前述の通り)。CVE が公表されても lock ファイルが更新されるまでの間、デプロイ環境では脆弱なバージョンがそのままインストールされ続けます。この CI audit はそのギャップを埋める役割を担っており、後述の Dependabot と組み合わせることで、検知から修正までの流れをカバーしています(下図参照)。

composer installのCVEギャップとCI audit・Dependabotによる補完

2. composer.lock の厳密管理

composer.lock はすべてのリポジトリでコミット対象にしています。composer install は lock ファイルを優先するため、意図しないバージョン変動を防げます。

ライブラリやフレームワーク系のリポジトリでは lock ファイルをコミットするかどうか議論になることがありますが、セキュリティの観点では「再現性の確保」が最優先です。

3. Dependabot による lock ファイルの自動更新

CVE が公表されたとき、composer audit が「問題あり」として検知しても、その修正を lock ファイルに反映する作業が必要です。手動では抜け漏れが起きやすいため、Dependabot に任せています。

# .github/dependabot.yml(抜粋・ディレクトリは省略)
updates:
  - package-ecosystem: "composer"
    directory: "..."   # プロジェクト構成に合わせて指定
    schedule:
      interval: "daily"
    target-branch: "develop"
    versioning-strategy: lockfile-only
    cooldown:
      default-days: 14

versioning-strategy: lockfile-only を指定しているため、Dependabot は composer.json のバージョン制約には触れず、composer.lock のみを更新します。セクション2で述べた「lock ファイルを正とする」方針と一貫しています。

cooldowndefault-days: 14 により、リリース直後のパッケージへの自動更新 PR をある程度抑制できます。参照した調査記事によれば、7日では過去の攻撃の大多数を防げるものの、14日に延ばすことで防げる攻撃がさらに増えます。チームで検討の結果、よりセキュアな14日を採用しています。

4. lefthook による pre-commit チェック

CI だけでなく、ローカルでのコミット時点にも防衛ラインを引いています。lefthook の pre-commit フックで composer.lock が変更されたコミットを検知し、2 種類のチェックを走らせます。

4-1. composer audit によるCVE チェック

まず既知脆弱性のチェックです。CI と同じ composer audit --lockedをローカルでも実行し、問題があればコミット自体を止めます。

「CI でも同じことをやっているなら不要では?」と思うかもしれませんが、問題のある依存を抱えたコミットを事前に弾くことで、CI を待つ時間のロスや、他のメンバーへの影響を最小化できます。また、ローカルで即座にフィードバックが得られるため、原因の特定と修正も早くなります。

4-2. 公開日時チェックによる遅延更新制御

もう 1 つのチェックが、リリースされて間もないパッケージのコミットをブロックする仕組みです。composer.lock に記録されている各パッケージの time フィールドを参照し、基準日から N 日以内にリリースされたものがあればコミットを止めます。

# lefthook.yml(抜粋)
pre-commit:
  commands:
    composer-min-release-age-check:
      glob:
        - "**/composer.lock"  # プロジェクト構成に合わせてパスを指定
      run: |
        # 複数リポジトリで共有するため、チェックスクリプト本体は
        # 管理用リポジトリから gh CLI 経由でダウンロードして実行する
        SCRIPT=$(gh api "repos/{org}/{dev-env-repo}/contents/scripts/composer-min-release-age-check.sh?ref=master" \
          --jq '.content' | base64 -d)

        HAS_ERROR=0
        for file in {staged_files}; do
          echo "$SCRIPT" | bash -s "$file" "$(date +%Y-%m-%d)" 14 || HAS_ERROR=1
        done
        [ "$HAS_ERROR" -eq 0 ]

スクリプトの中身は jqcomposer.lockpackages[] を走査し、各パッケージの time フィールドを基準日から 14 日前のタイムスタンプと比較しています。

# チェックスクリプトの核心部分(※ このスクリプトは macOS(BSD 版 date)での動作確認です。)
COMPARISON_DATE=$(date -j -f "%Y-%m-%d" -v-${MIN_RELEASE_DAYS}d "$BASE_DATE" "+%Y-%m-%d")
COMPARISON_TIMESTAMP=$(TZ=UTC date -j -f "%Y-%m-%d %H:%M:%S" "$COMPARISON_DATE 00:00:00" "+%s")

while IFS=$'\t' read -r package_name version time; do
  time_formatted=$(echo "$time" | sed 's/\([+-][0-9][0-9]\):\([0-9][0-9]\)$/\1\2/')
  time_timestamp=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "$time_formatted" "+%s" 2>/dev/null)

  if [ "$time_timestamp" -gt "$COMPARISON_TIMESTAMP" ]; then
    ERROR_PACKAGES+=("${package_name}|${version}|${time}")
  fi
done < <(jq -r ".packages[] | \"\(.name)\t\(.version)\t\(.time)\"" "$COMPOSER_LOCK_FILE_PATH")

*1

問題のあるパッケージが検出されると、以下のようなメッセージを出してコミットを止めます。

エラー: composer.lock の下記のパッケージは公開されて十分な日数が経過していません。
  基準日: 2026-04-10
  最低必要経過日数: 14日
  -------------------------
  パッケージ: vendor/package-name
  バージョン: 1.2.3
  バージョンの公開日時: 2026-04-08T12:00:00+09:00
  -------------------------
ダウングレードするか、どうしても入れる必要がある場合は git commit --no-verify でチェックをスキップしてください

緊急でパッケージを入れる必要がある場合は git commit --no-verify でスキップできます。その場合でも後続の CI の composer audit がセーフティネットとして機能します。

また、スクリプト本体を管理用リポジトリに置いて gh CLI 経由で取得する構成にしているのは、複数のリポジトリで同じチェックロジックを使い回すためです。スクリプトを 1 か所で管理できるため、閾値の変更や修正を全リポジトリに一括で反映できます。

lefthookによる遅延更新チェックの仕組みと限界

ただ、この仕組みには現時点での限界が

チェックに使っているのは composer.lock に記録されたパッケージの time フィールドです。これはパッケージ作者が申告した日時であり、攻撃者がこの日時を過去の日付に偽装すれば、チェックをすり抜けられてしまいます。

つまり、CVE などの既知脆弱性への対策としては有効ですが、axios で発生したような「悪意ある新規バージョンの公開」への完全な対策にはなりません。この課題はエコシステム全体の問題でもあります。Composer コミュニティで議論されている「Packagist サーバーサイドで付与する改ざん不能な登録日時」が実現すれば、根本解決を期待できます。まだ具体的な動きはコミュニティでは見受けられませんが、引き続き Composer 2.11 のリリースを注視しています。

ですので、この限界についてはチームに明示した上で運用しています。完璧でないことを承知の上で導入しているからこそ、他の防御層との組み合わせを大事にしています。


まとめ:今できることを重ねていく

Composer を使う PHP プロジェクトにおける対策を整理すると、以下のようになります。

対策 防げるもの 私たちの状況
composer audit の CI 組み込み(PRゲート) CVE・既知脆弱性 対応済み
composer.lock のコミット管理 意図しないバージョン変動 対応済み
Dependabot(cooldown 設定あり) CVE・依存の陳腐化 対応済み *2
lefthook pre-commit(公開日時チェック) 新しすぎるリリースの取り込み 対応済み *3
Composer 2.10 以降へのアップデート 既知マルウェアのフィルタリング 対応済み
Composer 2.11 の CoolDown(予定) 悪意ある新規リリース(改ざん耐性あり) リリース待ち

「Composer でできること」と「Composer 外で補完すること」を組み合わせながら、現実的な防衛ラインを引いています。本記事で紹介した取り組みはあくまでアプリケーション層の 1 つの層であり、インフラやネットワーク、定期的な脆弱性診断など、複数の層が重なることで全体のリスクを下げています。

完璧な対策はありません。ただ、攻撃者が「面倒だな」と感じるハードルを一段ずつ高くしていくこと、そしてその判断根拠をチームで共有し続けることが、信頼できるプロダクトを作る上での土台だと考えています。

Composer 2.11 の CoolDown リリースや Packagist 側のメタデータ整備など、エコシステム全体の動きにも即座に対応できるよう、引き続き追っていきます。同じところで悩んでいる方がいたら、ぜひ感想とかいただけると嬉しいです。


参考

*1:※ macOSでの動作を確認しています。Linux 環境では date -d への書き換えが必要です。

*2:※ 公開日時の改ざんには無力。CVE 対策として有効。

*3:※ 公開日時の改ざんには無力。CVE 対策として有効。