2026年に報告されたCVEは、1日あたり前年比131%増加しています。

PHPセキュリティ

PHPセキュリティ:コード、Composerの依存関係、ビルドでPHPの脆弱性を防ぐ

PHPを守るには3つの層があります。自分で書くコードでは、入力を検証し、パラメーター化クエリとフレームワークのエスケープを使い、信頼できないデータは決してunserializeしません。依存するComposerパッケージでは、composer auditを実行し、composer.lockをコミットし、安全でないバージョンをブロックします。それらをインストールするCIでは、実際にインストールされたものを記録し、ビルドの接続先を制限します。多くのPHPセキュリティガイドは最初の層で終わりますが、このガイドは3つすべてを扱います。

更新日

  1. 自分のコード インジェクション、XSS、デシリアライズ、ファイル処理、セッション フレームワークの既定設定、コードレビュー、PHPStan、Psalm、Semgrep
  2. Composerの依存関係 既知の脆弱性、放棄されたパッケージと悪意のあるパッケージ、lockfileのずれ、インストール時に動くコード composer audit、Composerのポリシー、DependabotまたはRenovate
  3. ビルド CIが実際にインストールして実行したもの、取得元、到達できた先 ピン留めしたアクション、最小権限のトークン、エグレスポリシー、ビルド時SBOM
PHPセキュリティの3つの層と、それぞれに効くツール

このページの内容:PHP2Shell、今も重要なPHPの脆弱性、設定とサポート対象バージョン、静的解析、Composerの依存関係のセキュリティ、PHPのサプライチェーン攻撃、CIのビルド、SBOM、層ごとのツール。

PHP2Shell:今すぐパッチを当てるべきPHPの脆弱性

2026年10月21日、PHPプロジェクトはPHP 8.5.12、8.4.27、8.3.36、8.2.35をリリースし、認証なしでのリモートコード実行につながりうる、PHP本体の2つの重大な脆弱性を修正しました。どちらもCVSS v4.0で9.2と評価されており、2つを合わせてPHP2Shellと呼ばれています。

  • CVE-2026-109618:メモリ不足の処理によって、リクエストデータが一部しか初期化されていない状態でPHPが中断されることがあり、その後のリクエストのクリーンアップが無効なデータを有効なものとして扱う可能性があります。
  • CVE-2026-109619:リクエストが所有する状態が、そのメモリの解放後も残ることがあり、同じワーカーが処理する次のリクエストがそれを使う可能性があります。

PHPのアドバイザリによれば、Webリクエストを処理するすべてのインストールが危険にさらされており、PHP-FPM、mod_phpを使うApache、php-cgi、FrankenPHP、LiteSpeedも含まれます。中でもmod_phpのリスクが最も高いとされています。PHP 8.1以前には、PHPプロジェクトから新しいリリースは提供されません。ただし深刻度を踏まえ、PHPセキュリティチームは、ソースからPHPをビルドする人やLTSベンダーを利用できない人向けに、PHP 7.4、8.0、8.1への最小限のソースバックポートを公開しました。一部のディストリビューションやLTSベンダーも、独自にバックポートした修正を提供しています。対応は次のとおりです。

  • 修正済みのリリースに更新してください。8.1以前の場合は、サポート対象のブランチに移行するか、ベンダーのバックポート済みパッケージを使うか、PHPを自分でビルドしているなら公式のソースバックポートを適用してください。
  • PHPを含むすべてのコンテナイメージを再ビルドし、デプロイし直してください。古いイメージタグは置き換えられるまで古いPHPを動かし続けるため、リポジトリにあるものだけでなく、デプロイされているものを確認してください:docker run --rm IMAGE php -v。
  • 今日中に更新できない場合は、PHPのセキュリティアドバイザリにある暫定的な緩和策を適用してください。たとえばsession.upload_progress.enabled=0です。これはphp.ini、FPMプール、またはリクエストの解析前に適用されるサーバー設定で指定し、すべてのワーカーを再起動します。ini_set()やFPMの.user.iniには頼らないでください。アドバイザリはどちらも使わないよう警告しています。PHPは、これらの緩和策は露出を減らすものの、インストールを安全にするわけではないと明言しています。

ここでcomposer auditが教えてくれないことに注意してください。PHP2ShellはComposerパッケージではなくランタイムにあるため、composer.lockに現れることはありません。実行しているPHPはベースイメージかOSパッケージから来ており、これはこのガイドの3つ目の層にあたります。バージョン表、タイムライン、すべてのイメージタグを確認するスクリプトについては、PHP2Shell:何にパッチを当て、脆弱なPHPをどう見つけるかをご覧ください。

今も重要なPHPの脆弱性

PHPアプリケーションの脆弱性の種類は、この10年ほとんど変わっていません。変わったのは、Laravel、Symfony、そして現代のPHPがその大半を既定で塞いでいることです。そのため実際のバグは、コードがそうした既定を回避する箇所に集中しています。OWASP Top 10:2025ではインジェクションがA05で、Software Supply Chain FailuresがA03に新たに入りました。後者はこのガイドの後半のテーマです。

脆弱性 PHPでの現れ方 修正方法 LaravelとSymfony
SQLインジェクション リクエストデータをSQLに連結する。生のクエリを書ける抜け道も含む パラメーターをバインドしたPDOのプリペアドステートメント EloquentとDoctrineはパラメーターをバインドする。whereRaw()、DB::raw()、連結したDQLはしない
クロスサイトスクリプティング(XSS) ユーザー入力をエスケープせずにHTMLへ出力する 出力時にhtmlspecialchars()またはテンプレートエンジンでエスケープする Bladeの{{ }}とTwigは既定でエスケープする。{!! !!}と|rawはしない
クロスサイトリクエストフォージェリ(CSRF) 状態を変更するフォームやAJAXエンドポイントが、セッションCookieを伴うリクエストをすべて受け付ける セッションごとのトークンをPOST、PUT、PATCH、DELETEのたびに検証し、SameSite Cookieも使う Laravelはwebルートでトークンを検証する(フォームでは@csrf)。Symfony Formsは既定でトークンを含めて検証する。検証から除外したルートは無防備になる
eval()によるコードインジェクション リクエストデータや、ユーザーが編集できるテンプレートや数式がeval()に渡る eval()を使わない。入力を決まった操作の集合に対応付けるか、必要な式にはパーサーを使う 自前のeval()呼び出しを守るフレームワークはない
オブジェクトインジェクション 攻撃者が書き込めるCookie、リクエストデータ、キャッシュエントリにunserialize()を使う 外部からのデータにはJSONを使う。unserializeが必要ならオブジェクトを拒否する 自前のunserialize()呼び出しを守るフレームワークはない
ファイルインクルードとアップロード 入力から組み立てたパスをincludeに渡す。アップロードをクライアントのファイル名のままWebルート配下に保存する インクルードするファイルを許可リストで限定し、アップロードは生成した名前でWebルートの外に保存する Laravelのstore()はファイル名を生成する。型とサイズはサーバー側で検証する
型の自動変換(type juggling) トークンやハッシュを==で比較し、"0e123" == "0e456"がtrueになる ===、シークレットにはhash_equals()、パスワードにはpassword_verify() フレームワークのヘルパーは安全に比較する。自前の比較は自分の責任
弱いセッションとCookie セッションIDがJavaScriptから読める、HTTPで送られる、ログイン後も変わらない Secure、HttpOnly、SameSiteのCookie、strictモード、ログイン後の新しいセッションID どちらもログイン時にセッションIDを再生成する
サーバーサイドリクエストフォージェリ(SSRF) ユーザーが指定したURLにfile_get_contents()やcURLを使う。file://やphar://などのラッパーも含む スキームとホストを許可リストで限定し、内部アドレスをブロックし、使わない場合はallow_url_fopenをオフにする 既定の保護はない
コマンドインジェクション 入力をexec()、shell_exec()、system()、proc_open()に渡す シェルを避け、引数を配列で渡すか、各引数をescapeshellarg()でエスケープする Symfony ProcessとLaravelのProcessファサードは引数の配列を受け付ける
マスアサインメント User::create($request->all())によって、リクエストがis_adminを設定できてしまう 代入可能なフィールドを列挙するか、検証済みの入力だけを渡す Eloquentのモデルは既定で保護されている。$guarded = []でその保護が外れる

SQLインジェクション:脆弱な例と修正例

// Vulnerable: the id goes straight into the SQL string
$rows = $pdo->query("SELECT * FROM orders WHERE id = " . $_GET['id']);

// Fixed: a prepared statement with a bound parameter
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ?');
$stmt->execute([$_GET['id']]);
$rows = $stmt->fetchAll();

// Laravel: whereRaw() takes SQL as written, where() binds the value
Order::whereRaw("status = '" . $request->input('status') . "'")->get(); // vulnerable
Order::where('status', $request->input('status'))->get();             // fixed

XSS:脆弱な例と修正例

<?php // Plain PHP: escape on output
echo '<p>' . $comment . '</p>';                                              // vulnerable
echo '<p>' . htmlspecialchars($comment, ENT_QUOTES, 'UTF-8') . '</p>';    // fixed ?>

{{-- Blade: {{ }} escapes, {!! !!} prints raw HTML --}}
<p>{!! $comment !!}</p>   {{-- vulnerable --}}
<p>{{ $comment }}</p>     {{-- fixed --}}

{# Twig: autoescape is on, |raw turns it off #}
<p>{{ comment|raw }}</p>  {# vulnerable #}
<p>{{ comment }}</p>      {# fixed #}

オブジェクトインジェクション:脆弱な例と修正例

// Vulnerable: an attacker-controlled cookie becomes PHP objects, and any
// class with a __wakeup() or __destruct() method in vendor/ is a gadget
$prefs = unserialize($_COOKIE['prefs']);

// Fixed: JSON carries data, not objects
$prefs = json_decode($_COOKIE['prefs'], true, 512, JSON_THROW_ON_ERROR);

// If you must read serialized data, refuse objects
$prefs = unserialize($data, ['allowed_classes' => false]);

pharデシリアライズ、つまりphar://パスへのファイル操作でアーカイブのメタデータがunserializeされていた手口は、PHP 8.0で範囲が狭まりました。pharのメタデータはpharを開いたときにはunserializeされなくなり、コードがgetMetadata()を呼んだときだけ処理されます。PHP 7から移行すべき理由がまた1つ増えたことになります。

ファイルインクルード:脆弱な例と修正例

// Vulnerable: ?page=../admin/reset reaches any .php file on the disk
include __DIR__ . '/pages/' . $_GET['page'] . '.php';

// Fixed: only names on an allow-list can be included
$pages = ['home' => 'home.php', 'pricing' => 'pricing.php'];
$file = $pages[$_GET['page'] ?? 'home'] ?? 'home.php';
include __DIR__ . '/pages/' . $file;

設定とランタイムの堅牢化

安全なアプリケーションでも、ランタイムを通じて情報が漏れることがあります。次のphp.iniの設定はOWASP PHP Configuration Cheat Sheetに沿っています。

設定 本番での値 理由
display_errors Off(log_errors = Onと併用) スタックトレースはパス、クエリ、ときにはシークレットまで表示する
expose_php Off レスポンスヘッダーで正確なPHPのバージョンを公表しなくなる
allow_url_include Off リモートURLのincludeを防ぐ。PHP 7.4から非推奨
allow_url_fopen アプリケーションがファイル関数でURLを取得しないならOff file_get_contents()経由のSSRFを狭める
disable_functions たとえばexec, shell_exec, system, passthru, proc_open, popen コードを実行できた攻撃者からシェルを取り上げる。アプリケーションが一切呼ばない関数だけを列挙する
open_basedir アプリケーションのディレクトリ PHPのファイル関数が開けるパスを制限する。分離の境界ではなく、ガードレールにすぎない
セッションCookie session.cookie_secure = 1、session.cookie_httponly = 1、session.cookie_samesite = LaxまたはStrict、session.use_strict_mode = 1 セッションIDを平文のHTTPとJavaScriptから遠ざけ、サーバーが発行していないIDを拒否する
file_uploads アプリケーションがアップロードを受け付けないならOff 入力経路を丸ごと1つなくす

サポート対象のPHPバージョンを使い続ける

php.netによると、PHPは各ブランチに2年間のアクティブサポートと、その後2年間のセキュリティ修正のみの期間を設けています。PHP 8.1は2025年12月31日にサポートが終了し、PHP 8.2も2026年12月31日に終了します。サポートが終了したブランチにはPHPプロジェクトから新しいリリースが提供されません。PHP2Shellで8.1のユーザーが置かれたのは、まさにこの状況です。公式の修正はソースバックポートだけでした。

  • PHP 8.1 サポート終了, 2025年12月31日に終了
  • PHP 8.2 セキュリティ修正のみ, 2026年12月31日まで
  • PHP 8.3 セキュリティ修正のみ, 2027年12月31日まで
  • PHP 8.4 アクティブサポート, 2026年12月31日まで
  • PHP 8.5 アクティブサポート, 2027年12月31日まで
PHPのサポート対象バージョン(日付はphp.netより)

LaravelとSymfonyの本番設定

  • デバッグをオフにする。LaravelではAPP_DEBUG=false、SymfonyではAPP_ENV=prodとAPP_DEBUG=0です。本番のデバッグページは設定と環境変数の値を表示してしまいます。
  • 鍵とシークレット。APP_KEY(Laravel)とAPP_SECRET(Symfony)はリポジトリに入れず、漏れたらローテーションしてください。LaravelのAPP_PREVIOUS_KEYSを使うと、全ユーザーをログアウトさせずに鍵をローテーションできます。
  • 信頼するプロキシ。ロードバランサーの背後では、信頼するプロキシを列挙してください(Laravelのtrusted proxiesミドルウェア、Symfonyのframework.trusted_proxies)。そうすればクライアントがX-Forwarded-*ヘッダーでIPアドレスやHTTPSを偽装できなくなります。

PHPの静的解析(SAST)

静的解析は、コードを実行せずに読み取ります。ほとんどのPHPチームは、次の3つのオープンソースツールでカバーできます。

  • PHPStanは、段階的に厳しくなるレベルで型エラーやバグを見つけます。セキュリティスキャナーではありませんが、多くのセキュリティバグは型の取り違えや未チェックのnullから始まります。
  • Psalmはテイント解析を備えています。$_GETや$_COOKIEなどのソースから、echo、include、SQLクエリなどのシンクまでデータを追跡し、TaintedSqlやTaintedHtmlといった問題を報告します。現在のPsalmは型チェックと一緒にこれを実行します。--taint-analysisで1回の実行だけ有効にでき、--report=results.sarifでGitHub code scanning用のSARIFを出力できます。
  • SemgrepはPHPを一般提供の言語としてサポートしており、PHPのよくある誤りに対するコミュニティルールと、商用版での関数をまたぐデータフロー解析を提供しています。

レガシーなPHPでSASTのノイズが多いのは構造的な理由によります。動的なinclude、可変変数、__get()や__call()などのマジックメソッド、そしてアナライザーが追えないコンテナやファサードにデータを通すフレームワークの規約です。ツールは推測して誤検知を出すか、諦めて実際のフローを見逃すかのどちらかになります。

古いコードベースへの導入は、ラチェットのように進めるのが最善です。今日の検出結果をベースラインとして記録し(vendor/bin/phpstan analyse --generate-baselineはphpstan-baseline.neonを書き出し、Psalmには--set-baselineがあります)、新しい検出結果だけでビルドを失敗させ、ベースラインを減らしながらレベルを上げていきます。長く運用されているPHPコードベースを持つチームからは、AIによる自動修正は依存関係の更新ではうまく機能する一方、コードの検出結果では出来にむらがあると聞いています。SASTの検出結果に対して生成された修正は、ほかの変更と同じように注意深くレビューしてください。SASTとSCAは異なる問いに答えます。SASTとSCAの比較をご覧ください。

Composerの依存関係のセキュリティ

LaravelやSymfonyのアプリケーションは数十のComposerパッケージをインストールし、その大半は推移的な依存関係です。Composerはここ数回のリリースで本格的なセキュリティ機能を備えるようになりました。このセクションは現行リリースのComposer 2.11に基づいています。

composer audit

composer auditは、インストール済みのパッケージをPackagist.orgのAPIのセキュリティアドバイザリと照合します。このAPIは、GitHub Advisory DatabaseやFriendsOfPHP/security-advisoriesなどのソースからアドバイザリを集めています。放棄されたパッケージも報告し、Composer 2.10からはマルウェアとしてフラグが付いたパッケージバージョンも報告します。すべてのオプションはComposerのドキュメントをご覧ください。

composer audit                      # audit what is installed in vendor/
composer audit --locked             # audit composer.lock, whatever is in vendor/
composer audit --no-dev             # skip require-dev packages
composer audit --direct             # direct dependencies only (Composer 2.11)
composer audit --format=json        # table, plain, json or summary
composer audit --abandoned=report   # ignore, report or fail on abandoned packages
composer audit --ignore-severity=low

Composer 2.10からは、問題がなければ終了コードは0、何かが監査に通らなければ1になるため、出力を解析しなくてもCIのゲートとして使えます。放棄されたパッケージは既定で監査を失敗させます。

受け入れた検出結果は、誰も覚えていないCIのフラグではなく、理由とともにcomposer.jsonに書きます。Composer 2.10は、古いaudit設定の大部分をpolicyブロックに置き換えました。

{
    "config": {
        "policy": {
            "advisories": {
                "ignore-id": {
                    "CVE-1234": "Not affected: we never call the vulnerable parser."
                }
            },
            "abandoned": {
                "audit": "report"
            }
        }
    }
}

従来のキー(audit.ignore、audit.abandoned、audit.block-insecure)は、対応するpolicyセクションがない間はフォールバックとして機能しますが、非推奨です。アドバイザリ関連の設定はまとめて移行してください。policy.advisoriesが存在した時点で、アドバイザリ関連のaudit.*キーはすべて無視されます。

安全でないバージョンをブロックする

Composer 2.9(2025年11月)から、composer updateとcomposer requireはセキュリティアドバイザリのあるパッケージバージョンを拒否します。以前はaudit.block-insecureという設定で、現在はpolicy.advisories.blockとなり、既定で有効です。Composer 2.10ではPackagistのマルウェアフィードに基づくマルウェアのブロックが加わり、これはcomposer installにも適用されます。Composer 2.11では任意のcooldownポリシーが加わり、新しくリリースされたバージョンを"7 days"のような指定期間だけ保留できます。悪意のあるバージョンの多くは数時間から数日で発見され、削除されるためです。

CIでは2つの点が重要です。第1に、アドバイザリによるブロックはバージョン解決時に適用されます。composer.lockにすでにあるバージョンは、アドバイザリの公開後もcomposer installでインストールされるため、ゲートとしてのcomposer auditは引き続き必要です。第2に、--no-blockingは1つのコマンドですべてのポリシーをオフにします。パイプラインで使われていないか探す価値があります。

Composer 2.9より前は、安全でないバージョンをブロックする一般的な方法はroave/security-advisoriesでした。GitHub Advisory DatabaseとFriendsOfPHPをもとに、既知の脆弱なバージョンすべてとconflictを宣言するパッケージです。今も動作し、古いComposerでは今も役立ちます。Composer 2.9以降では、組み込みのブロックが追加パッケージなしで同じアドバイザリをカバーします。

lockfileの規律

  • アプリケーションではcomposer.lockをコミットし、すべての環境が同じバージョンをインストールするようにします。
  • CIとデプロイではcomposer installを使い、その場で新しいバージョンを解決するcomposer updateは決して使いません。
  • 本番ビルドは--no-devでインストールし、テスト用のツールが出荷されないようにします。
  • composer validate --strictを実行します。composer.lockがcomposer.jsonに対して古い場合に失敗します。

lockfileはComposerが解決した内容を記録します。lockfileが記録しないものをご覧ください。

インストール時に動くコード:プラグイン、スクリプト、オートロードされるファイル

Composerは、多くのチームが想定するより多くの場所で依存関係のコードを実行します。そのためComposerは、npmのインストールスクリプトに相当するPHPの仕組みと言えます。

  • プラグインはComposer自体の中で動きます。Composer 2.2からは、allow-pluginsの設定に実行を許可するプラグインをすべて列挙する必要があり、既定では何も許可されません。そこへの新しいエントリは、シェルにアクセスできる新しい依存関係として扱ってください。
  • スクリプトはルートのcomposer.jsonからのみ実行され、依存関係からは実行されません。ただし、多くの場合アプリケーションを起動します。
  • オートロードされるファイルは、オートローダーが読み込まれるたびに実行されます。標準的なLaravelアプリケーションでは、composer installの最後にphp artisan package:discoverが実行され、これがオートローダーを読み込むため、依存関係がオートロードするfilesはインストール中にCIのジョブの中で実行されます。

ジョブにプラグインやスクリプトが不要なら、--no-plugins --no-scriptsでインストール中に依存関係のコードが動かないようにし、必要な手順は後から明示的に実行してください。

リポジトリのソースと依存関係の混乱

Composerは、Packagist.org、Private Packagistのような非公開のComposerリポジトリ、VCSリポジトリ(GitのURL)、ローカルパスからインストールできます。非公開と公開のソースを混在させると、依存関係の混乱(dependency confusion)の余地が生まれます。非公開パッケージと同じ名前がPackagist.orgにより高いバージョンで存在したら、どちらが入るのでしょうか。Composer 2では既定ですべてのリポジトリが正規(canonical)です。Composerはあるリポジトリでパッケージを見つけると、リストのそれより下では探さず、Packagist.orgは常に最後になります。リポジトリに"canonical": falseを設定するとこの保護が外れるため、意図した場合にだけ設定してください。VCSリポジトリはGitホストがタグに対して返すものをそのまま取得するため、lockfileでコミット単位に固定し、その変更をレビューしてください。

放棄されたパッケージと保守されていないパッケージ

レガシーなPHP環境には、メンテナーが離れたパッケージが残っています。Packagistはメンテナーが申告するとパッケージを放棄済みとしてマークし、多くの場合は代替を示します。composer auditは既定でそれを失敗として扱います。ただし、保守されていないパッケージの多くは正式に放棄されることがありません。入力、認証、暗号を扱うものは最終リリース日を確認し、脆弱性に迫られる前に置き換えを計画してください。

テストが少ないときに安全に更新する

最も難しいのは、脆弱な依存関係を抱え、テストの少ない大規模なレガシーアプリケーションです。有効な方法は次のとおりです。

  1. 脆弱なパッケージを引き込んでいるパッケージを更新する。検出結果の大半は推移的です。composer why vendor/packageを実行して依存の連鎖を確認し、まず上流のパッケージを制約の範囲内で更新します。
  2. 一度に1つずつ更新する。composer update vendor/package --with-dependenciesなら、composer.lockの差分でレビューできるほど変更を小さく保てます。
  3. まず重要な経路をスモークテストする。ログイン、決済、主要なAPIエンドポイントなど、少数のエンドツーエンドのチェックで、更新による破損の大半を捉えられます。
  4. 更新が触れる部分の周りに特性テストを追加する。コードが今どう動くかを記録しておけば、更新で変わったときに気づけます。
  5. 段階的にロールアウトし、すべての顧客に届く前にエラー率を監視します。

composer auditが見落とすもの

lockfileは計画であって、ビルドの記録ではありません。composer auditはvendor/かcomposer.lockを読むため、次のものは見えません。

  • PHP本体とその拡張。これらはベースイメージかOSパッケージから来ます(PHP2Shellがその一例です)。
  • DockerイメージにインストールされたPECL拡張とシステムライブラリ。
  • CIがcurlで取得するツール、実行するアクション、プラグインやスクリプトがダウンロードするバイナリ。
  • まだアドバイザリもマルウェアのフラグもない悪意のあるコード。

同じ限界は、それぞれのエコシステムのnpm auditとpip-auditにも当てはまります。

PHPのサプライチェーン攻撃

PHPに対するサプライチェーン攻撃が実際にどこを突くのか、そして何がそれぞれを防げたのかを、4つのインシデントが示しています。

2021年3月:php-srcへの悪意のあるコミット

2021年3月28日、2人のコア開発者の名前でphp-srcリポジトリに悪意のあるコミットが2件プッシュされました。PHPチームは、自前のGitサーバーの運用をやめ、GitHubのリポジトリを正規とすることを発表しました。その後の続報では、コミットはサーバーの侵害によるものではなく、HTTPSとパスワード認証でプッシュされており、漏えいしたユーザーデータベースの認証情報が使われた可能性があるとされました。何が防げたか:自分のパイプラインでは何も防げません。標的は言語そのものであり、PHPのランタイムも依存関係であることを思い出させる事例です。

2021年4月:Composerのコマンドインジェクション(CVE-2021-29472)

ComposerによるMercurialリポジトリのURLの扱いに欠陥があり、コマンドインジェクションが可能でした。Packagistの報告によると、Packagist.orgとPrivate Packagistは細工したパッケージURLを通じて理論上さらされていましたが、悪用されたとは考えておらず、Composer 2.0.13と1.10.22で修正されました。何が防げたか:開発者のマシンとCIイメージのComposer自体を最新に保つことです。パッケージマネージャーもサプライチェーンの一部です。

2023年5月:Packagistのアカウント乗っ取り

2023年5月1日、攻撃者はほかのサイトで漏えいしたパスワードを使い、Packagist.orgの休眠アカウント4件にログインしました。これらのアカウントは合わせて14のパッケージを管理しており、攻撃者はそれらのパッケージをフォークしたリポジトリに向けました。フォークは説明文が変えられていただけで、悪意のあるコードは含まれていませんでした。Packagistは翌朝までに元に戻しました。何が防げたか:コミットされたcomposer.lockです。composer installはlockfileに記録されたソースを使うからです。加えて、Packagist自身が推奨するように、予期しないURLがないかlockfileの差分をレビューすることです。メンテナー側では、使い回さないパスワードと二要素認証が有効です。

2026年:laravel-langとintercom-phpで乗っ取られたタグ

2026年、盗んだアクセス権を持つ攻撃者が、既存パッケージの悪意あるリリースを公開しました。Packagistはサプライチェーンセキュリティに関する続報の中で、intercom/intercom-php(4月30日)とlaravel-langのパッケージ(5月22日)を挙げています。laravel-langの件では、バージョンタグが悪意のあるフォークのコミットを指しており、ペイロードであるsrc/helpers.phpファイルがComposerのオートローダーによって読み込まれ、クラウドの鍵、SSH鍵、.envファイルを狙う認証情報窃取ツールを取得していました。Aikidoは侵害されたバージョンを233件数えています。何が防げたか:フラグが付いた後は、Composer 2.10のマルウェアポリシーがcomposer installでもそれらのバージョンをブロックします。それ以前の段階では、cooldown期間が新しすぎるバージョンを締め出し、エグレスポリシーがペイロードのサーバーへの到達を止め、各ビルドの記録がどのビルドに不正なバージョンが入ったかを示します。

その後Packagistは、タグを付け替えた安定版バージョンを拒否すること、所有者とタグの変更の透明性ログを公開すること、そして2026年3月からPackagist.orgでマルウェアにフラグを付けていることを発表しています。

CIでPHPのビルドを守る

ビルドはコード、依存関係、シークレットが一堂に会する場所であり、悪意のあるパッケージが最も大きな被害を与える場所です。本番ビルド用のGitHub Actionsワークフローの例です。

name: build
on: [push, pull_request]

permissions:
  contents: read            # least privilege for GITHUB_TOKEN

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0
        with:
          php-version: "8.4"
          tools: composer:v2
          coverage: none

      - run: composer validate --strict

      - id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
      - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: composer-${{ hashFiles('composer.lock') }}

      - run: composer install --no-dev --no-interaction --no-progress --optimize-autoloader
      - run: composer audit --no-dev
  • 最小権限のトークン。permissions: contents: readにより、侵害されたステップがGITHUB_TOKENでコードやリリースをプッシュできなくなります。GitHubトークンの権限をご覧ください。
  • ピン留めしたアクション。完全なコミットSHAはタグのように付け替えられません。コメントによって、DependabotやRenovateからもバージョンが読み取れます。
  • vendor/ではなくダウンロードをキャッシュする。Composerのダウンロードキャッシュをキャッシュすれば、どのジョブでもcomposer installがlockfileに対して実行されます。復元されたvendor/はそのまま信用されてしまい、キャッシュはワークフローをまたいで汚染されることがあります。GitHub Actionsのキャッシュをご覧ください。
  • インストールされたものを記録する。lockfileは何がインストールされるべきかを示します。ビルドから記録したSBOMは、PHPのランタイム、OSパッケージ、Composer以外のダウンロードを含め、実際に何がインストールされたかを示します。ビルド時SBOMをご覧ください。
  • ビルドの接続先を制限する。Composerのビルドに必要なのはPackagist、GitHub、そして自社の非公開リポジトリです。それらだけを許可するエグレスポリシーがあれば、認証情報窃取ツールが見つけたものを送信できなくなります。

PHPプロジェクトのSBOMを生成する

標準的なオープンソースの選択肢はCycloneDX Composerプラグインです。プラグインなので、allow-pluginsへのエントリが必要です。

composer require --dev cyclonedx/cyclonedx-php-composer
composer CycloneDX:make-sbom --output-format=JSON --output-file=sbom.json

このプラグインはcomposer.jsonを、インストール済みのパッケージまたはcomposer.lockと合わせて読むため、Composerの依存関係は正確に列挙しますが、それ以外は何も含みません。PHPのランタイム、その拡張、ベースイメージのaptやapkのパッケージ、ジョブがダウンロードしたツールは入りません。ビルド時SBOMはComposerのファイルではなくジョブが取得したものから作られるため、それらも記録します。形式やSBOMに含めるべき内容については、SBOMとはをご覧ください。

層ごとのPHPセキュリティツール

層 ツール カバーするもの 見落とすもの
コード PHPStan、Psalm、Semgrep バグと型エラー、汚染されたデータフロー、既知の安全でないパターン 脆弱な依存関係、ランタイム、ビルド
依存関係 composer audit、Composerのポリシー、roave/security-advisories、DependabotまたはRenovate 公開済みのアドバイザリ、放棄されたパッケージやフラグ付きのパッケージ、更新のプルリクエスト 自分のコード、そしてcomposer.lockの外にあるものすべて
イメージ コンテナスキャナー ビルド済みイメージ内のOSパッケージと言語パッケージ ビルドが取得して残さなかったもの、誰もスキャンしなかったイメージ
SBOM CycloneDX Composerプラグイン CycloneDX SBOMとしてのComposerパッケージ Composerの外にあるものすべて
ビルド CRACI 各GitHub Actionsジョブが、Composerとほかのあらゆるソースから実際にインストールしたもの。継続的に監視 自分のPHPコード(SASTなし)、GitHub Actions以外のCIシステム

イメージについて詳しくはコンテナスキャンをご覧ください。すべての層で検出結果を少なく、質の高いものにするには、脆弱性のノイズを減らす方法をご覧ください。

PHPセキュリティとCRACI

CRACIは、各ジョブが実際にインストールしたものを記録するGitHub Actionsランナーです。runs-onをcraciに変えるだけで、ジョブがダウンロードするすべてのComposerパッケージが、そのパッケージURL(pkg:composer/vendor/package)とともに記録されます。同じジョブ内のOSのパッケージマネージャーからのパッケージ、コンテナイメージ、Gitソース、その他のダウンロード、さらにCIキャッシュから復元されたパッケージも一緒に記録されます。SBOMのすべてのコンポーネントは、実際にビルドに含まれていたものです。

CRACIはそれらのコンポーネントを既知の脆弱性と照合し、新しいアドバイザリが公開されるたびに、出荷したもののSBOMを再チェックし続けます。そのため、来年公開されるCVEでも、それを含むビルドにたどり着けます。ポリシーゲートは、特定のCVEを含むビルドを含め、検出結果に応じてビルドを止められます。エグレスポリシーはジョブの接続先を制限します。

CRACIはPHPのコードをスキャンしません。設計上、SASTはありません。コードの層にはPHPStan、Psalm、Semgrepを使い続けてください。PHPのビルドがインストールするすべてのパッケージを確認する。

CRACIが答えにならない場合

  • FTPでデプロイしている、またはCIのない共有ホスティングを使っている。記録すべきビルドがありません。まずはcomposer auditとコミットしたcomposer.lockから始めてください。
  • リスクの大半が自分のコードにある。たとえば生のSQLだらけのレガシーアプリケーションです。SAST、レビュー、テストが先です。
  • GitHub Actions以外でビルドしている。GitLab CIやBitbucket Pipelinesの場合です。CRACIが現在サポートするのはGitHub Actionsで、GitLab CIとJenkinsはロードマップにあります。

PHPセキュリティ:よくある質問

PHPのコードはどう守ればよいですか?

3つの層で取り組みます。コードでは、入力を検証し、プリペアドステートメントを使い、フレームワークの出力エスケープに頼り、信頼できないデータは決してunserializeしません。依存関係では、composer.lockをコミットし、CIでcomposer auditを実行し、Composerによる安全でないバージョンのブロックを有効にしておきます。ビルドでは、アクションをピン留めし、トークンを制限し、ネットワークアクセスを絞り、ジョブがインストールしたものを記録します。

最も一般的なPHPの脆弱性は何ですか?

SQLインジェクション、クロスサイトスクリプティング、クロスサイトリクエストフォージェリ、eval()によるコードインジェクション、unserialize()によるオブジェクトインジェクション、ファイルインクルードと安全でないアップロード、==による型の自動変換、弱いセッション設定、URLラッパー経由のSSRF、コマンドインジェクション、マスアサインメントです。脆弱なComposerの依存関係や、PHP2Shellのようなパッチ未適用のPHPバージョンも、同じくらいよくあるインシデントの原因です。

最良のPHPセキュリティスキャナーは何ですか?

PHPセキュリティを1つでカバーできるツールはありません。コードにはPHPStan、Psalm、Semgrepなどの静的解析ツール、Composerの依存関係にはcomposer audit、イメージにはコンテナスキャナー、そしてCIが実際にインストールしたものにはビルド時の記録を使います。

composer auditはどう動き、何を見落としますか?

インストール済みのパッケージ、または--lockedを付けた場合はcomposer.lockを、Packagist.orgのAPIのアドバイザリと照合し、放棄されたパッケージとマルウェアのフラグが付いたパッケージを報告します。何かが監査に通らなければ1で終了します。PHP本体、PECL拡張、イメージ内のOSパッケージ、CIがComposerの外でダウンロードするものは見えません。

CIで安全でないComposerパッケージをブロックするには?

Composer 2.9以降を使ってください。updateとrequireの際にアドバイザリのあるバージョンを既定でブロックします(policy.advisories.block)。lockfileに固定された脆弱なバージョンはそのままインストールされるため、composer installの後にcomposer auditも実行し、その終了コードでジョブを失敗させてください。CIでは--no-blockingを決して使わないでください。

Laravelは既定で安全ですか?

おおむね安全です。Bladeは出力をエスケープし、Eloquentはクエリのパラメーターをバインドし、モデルはマスアサインメントから保護され、ログイン時にセッションIDが再生成されます。ただし、コードが{!! !!}、入力を渡したwhereRaw()、$guarded = []、本番でのAPP_DEBUG=trueを使った場合や、APP_KEYが漏えいした場合には、これらの既定の保護は効かなくなります。

テストの少ないレガシーなPHPアプリで依存関係を更新するには?

脆弱なパッケージを引き込んでいる上流のパッケージを、composer update vendor/package --with-dependenciesで1つずつ更新します。重要な経路をスモークテストし、更新が触れる部分の周りに特性テストを追加し、段階的にロールアウトします。

PHPプロジェクトのSBOMはどう生成しますか?

CycloneDX Composerプラグイン(cyclonedx/cyclonedx-php-composer)が、composer CycloneDX:make-sbomでComposerパッケージからSBOMを生成します。含まれるのはComposerパッケージだけです。ビルド時SBOMなら、PHPのランタイム、OSパッケージ、ビルド中のその他のダウンロードも記録します。

PHP2Shellとは何ですか?

PHP2Shellは、CVE-2026-109618とCVE-2026-109619の呼び名です。認証なしでのリモートコード実行につながりうる、PHP本体の2つの重大な脆弱性で、2026年10月21日にリリースされたPHP 8.5.12、8.4.27、8.3.36、8.2.35で修正されています。PHP2Shellをご覧ください。

PHPのビルドがインストールするすべてのパッケージを確認

GitHub ActionsのワークフローをCRACIで1つ実行し、記録されたSBOMをcomposer.lockと比べてみてください。

デモを予約