未来を形作るテクノロジーの深掘り記事。

PHP CVE対応で小細工より遮断を選ぶべき理由

重大なPHPの脆弱性と、パッチ公開前の猶予期間にチームはどう判断すべきか。悪用可能性の評価と迅速なパッチ適用の実践法を解説。

嵐の接近に備えて自発的に照明を落とす都市のビル群。事前の遮断を象徴するイメージ
ときには一番安全な設定は「オフ」です。待つあいだ無防備にさらされるより、組織ごと落とす選択をした機関もありました。

PHPプロジェクトが重大なセキュリティリリースの予告を出したとき――しかもパッチが実際に存在するより数日前でした――インターネットはいつもの反応を見せました。生産的なパニックです。オランダ政府のログインポータル、国営放送局、国会の公式サイトが、念のため先回りしてダウンしました。Cloudflareは慌ててWAFルールを組み直し、システム管理者たちはRedditで半端に検証されたphp.iniの緩和策をやり取りし、その間にも攻撃手順がSNSで出回っていました。そして、この騒動を眺めて得た私の逆張りの意見はこうです。全部まとめて落とした組織こそ、その場で最も賢い判断をしたのであり、巧妙な部分的緩和策を練っていたチームの多くは、ほぼセキュリティ・シアターをしていただけだと考えています。この主張には擁護が必要ですし、明確な限界もあります。では、重大なPHPのCVEの対応猶予期間が、パッチ戦略について実際に何を教えてくれるのか見ていきましょう。

事前予告されたパッチが諸刃の剣になる理由

脆弱性開示の通常の流れは、パッチが先で詳細は後です。今回はそれが逆転しました。流出した動画がPHPチームの判断を早めさせ、修正版より先に告知が出ました。実際のパッチ版(8.5.12、8.4.27、8.3.36、8.2.35)は、数日後の固定されたUTCの時刻にリリースされる予定でした。「重大な問題が存在すると確認した」段階と「修正版はこれです」の段階のあいだのこのギャップこそ、現代の脆弱性管理で最も危険な時期です。防御側も攻撃側も、同じ時計を見つめているからです。

多くの実務者は、パッチをすぐ出すべきだったと主張しました。しかし仕組みを考えてみてください。公開されたパッチは、実質的にはリバースエンジニアリングのキットです。修正前後のソースを比べれば、脆弱なコードパスはごく数行にまで絞り込めます。しかもLLMを使った攻撃コード生成によって、「差分が手に入る」から「動く攻撃コードができる」までの時間は、数日から数時間へと縮んでいます。PHPのリリースマネージャー自身も、告知でバグのクラスが明かされた時点で、パッチ前にもかかわらず攻撃コードがあれほど早く出回ったことに驚いたと認めています。早期リリースは世界を協調させるわけではありません。それは、運用者が寝ている、会議中である、あるいは変更管理の待ち行列で止まっている間に、何十万もの未対応インスタンスに対する攻撃タイマーを始動させるだけです。

固定されたリリース時刻は不格好に感じられたものの、実際には協調のための仕組みでした。全員が14:00 UTCにパッチを当てる。深刻な露出を抱える全員が、その前にサービスを止める。この同期された時間帯が、どの瞬間にもオンラインで脆弱な状態にあるサーバーの総数を最小化します。これはセキュリティ業界のあらゆる場面での協調的開示と同じ論理です。泥臭く、居心地も悪く、たいていは最もましな選択肢なのです。

情報がほとんどない中で悪用可能性を評価する

この猶予期間で最も難しかったのは、不完全なデータでのトリアージでした。アドバイザリーはあえて曖昧に書かれていたのに、チームは「停止する」「緩和する」「待つ」のどれかを決めなければなりませんでした。私が実際に使う枠組みはこちらです。バグの形ではなく、露出の形に主に注目するものです。

  • まず攻撃対象領域。初期の兆候はファイルアップロード処理とセッションのアップロード進捗を指していました。匿名ユーザーからのmultipart POSTリクエストを受け付けているなら(お問い合わせフォーム、アバター画像のアップロード、インポートツールなど)、あなたは列の先頭にいます。VPNとSSOの裏にある社内管理ツールは、まったく別のリスククラスです。
  • インタプリタの到達範囲。PHPコアのバグは10年間すべてのバージョンに影響するので、露出は「リクエストが届くすべてのPHPプロセス」にほぼ等しくなります。ここでは分析より棚卸しが重要です。多くの組織が、自分たちがPHPを動かしていることを忘れていた場所を発見しました。古いマイクロサイト、監視ダッシュボード、2019年に作ったマーケティング用のWordPressなどです。
  • 被害半径(ブラストレディウス)。ここで過去のアーキテクチャ上の罪が跳ね返ってきます。議論のなかである実務者が指摘したように、古典的なPHPアプリはユーザーと管理者で共有された、全データを読み書きできるデータベースロールで動いていることが多いのです。暴走したPHPプロセスは1つのアプリを乗っ取るだけでなく、データベース全体を外部に持ち出せます。アプリがテナントごとのロールと行レベルセキュリティを使っていれば、最悪のケースは劇的に小さくなります。
  • 検知の態勢。「このホストにWebシェルが現れたら気づけるか?」に答えられないなら、パッチの速さが唯一の防御手段です。これは、より広い死角とつながっています。多くのチームは認証イベントはログに残しますが、セッションが確立された後に何が起きたかはログに残していないのです。

この4つの視点で検討すれば、判断はたいてい自ずと決まります。露出が大きく、被害半径が広く、検知もできない? なら停止です。攻撃対象が狭く、権限が限定され、ログもしっかり取れている? WAFルールと注意深い監視で、パッチ適用の時間まで持ちこたえられるでしょう。

緩和策の罠:部分的な対策がたいてい期待を下回る理由

告知から数時間のうちに、フォーラムで一つの緩和策が広まりました。php.iniでセッションのアップロード進捗とファイルアップロードを無効にする、というものです。人々は律儀に設定し、phpinfo()で確認して、少し安心しました。するとPHPのリリースマネージャーが、INI設定だけでは守れない、PHP本体にパッチを当てる必要がある、と端的に述べました。これが緩和策の罠を一言で表しています。設定の調整は行動しているという手応えを与えますが、インタプリタのメモリ破壊系のバグに対しては、攻撃経路についての推測にすぎず、脆弱性そのものへの修正ではないのです。

実際に効く唯一の緩和策は、エッジでのリクエスト形状によるフィルタリングでした。攻撃にmultipartのリクエストボディが必要で、サイトがファイルアップロードを必要としないなら、そのコンテンツタイプを丸ごとブロックすれば、攻撃経路を妨害することを期待するのではなく、経路そのものを消せます。Apacheなら、次のようなrewriteルールになります。

# Block all multipart/form-data requests at the vhost level
RewriteEngine On
RewriteCond %{HTTP_CONTENT_TYPE} ^multipart/form-data [NC]
RewriteRule .* - [R=403,L]
# For nginx, the equivalent lives in the server block:
# if ($http_content_type ~* "^multipart/form-data") { return 403; }

この条件分岐に注目してください。ポイントはまさにここです。これは、アップロードが重要な機能でない場合にのみ有効です。アップロード中心のプロダクトを運用しているなら、自分でDDoSを起こしたようなものです。RCEよりはましかもしれませんが、それはセキュリティ上の判断ではなく事業上の判断です。また、これは攻撃の形に賭けた策にすぎません。第二の攻撃経路が現れれば(Cloudflareも新しい攻撃経路が出るたびにルールを更新したと報告しています)、そのルールは一気に役に立たなくなります。エッジでのフィルタリングは時間を稼ぐものであって、安全を買うものではありません。

多層の防御を備え、受信トラフィックをフィルタリングする城門。WAFルールとリクエストフィルタリングを象徴
エッジのフィルタリングは要所の関門であって、根治策ではありません。本当の修正を待つあいだに役立つだけです。

停止という選択肢の、居心地の悪い正当性

さて、冒頭で述べた主張に戻ります。議論に参加していたあるエンジニアは、修正が届くまで顧客のアプリを全部オフラインにするという判断を下し、その日は一日中電話で説明に追われたそうです。それは職業的に痛い決断です。しかし正しくもあります。理由はコストの非対称性が非常に大きいからです。数時間のダウンタイムは、悪い一日と気まずい請求書の話で済みます。一方で、インタプリタレベルのバグに対する修正前のRCEは、攻撃手順が公開され、LLMエージェントが大規模に攻撃をばらまける状況では、情報漏えいの報告、フォレンジックの対応、そしてそのホストが安全だと証明できないためにすべてのホストを一から作り直す必要があるのかという、非常に現実的な問いになります。

この最後の点は強調しておく価値があります。複数のコメント投稿者が、合理的な対応策は「ホストを焼いて作り直せ」だと主張しましたが、それは誇張ではありませんでした。共有を一切しないPHP-FPMホストでさえ、侵害後の永続化の手段は残ります。cron、改変されたファイル、汚染されたセッション、盗まれたデータベースの認証情報などです。攻撃コードの公開からパッチ適用までの期間にオンラインで未対応だったのなら、「パッチは当てた」では「侵入されたか」への答えにはなりません。既知の正常なイメージからの再構築こそが答えです。不変インフラを採用していたチームは、静かに素晴らしい一週間を過ごしました。パッチ済みのベースイメージから再デプロイし、認証情報をローテーションして、次に進んだのです。

では、停止という議論はどこで破綻するのでしょうか。3つあります。第一に、病院システム、決済フロー、緊急インフラなど本当に重要なサービスです。ダウンタイムそのものが安全上のコストを伴うため、こうしたものは電源スイッチではなく多層的な隔離が必要です。第二に、停止にかかる時間がパッチの猶予期間より長い組織です。リリースプロセスが3日がかりの手作業なら、そもそも14:00 UTCには間に合いません。第三に、そして最も耳の痛い点ですが、停止が機能するのは自分が何を動かしているかを全部把握しているときだけです。サブドメインに残された忘れられたWordPressは、誰もその存在を覚えていないからといって勝手に落ちてくれたりはしません。地味ではあっても、資産の棚卸しこそが実際の境界線なのです。

次の騒動に備えて、ランタイムの更新戦略を作る

次は必ず来ます。有益な問いは、今回の猶予期間をどううまく乗り切ったかではなく、次の猶予期間が退屈なものになっているかどうかです。レバレッジの大きい順に、いくつかの具体的な施策を挙げます。

  1. サポートされているPHPバージョンを追い、パッチ更新を自動化する。数分でパッチを当てられたチームは、タグを切るとベースイメージが再ビルドされ再デプロイされる仕組みを持っていました。PHPの更新に、ペットのように扱うサーバー上でのコンパイルが必要なら、既に競争に負けています。ディストリビューションのパッケージ(Debian、Alpine)は、リリースから数時間以内に修正版を配布しました。手作りのビルドより、ディストリビューションのセキュリティ更新のペースに乗るほうが勝ちです。
  2. レガシーなものの前面には、マネージドルール付きのWAFを置く。Cloudflareは、パッチが存在する前から、無料プランと有料プランの両方でマネージドWAFルールによりこれを緩和していました。WAFが一般に多層防御なのか見せかけなのかは議論できます。しかしパッチ前の猶予期間においては、間違いなく多層防御として機能します。
  3. 今すぐデータベースの被害半径を縮める。読み取りと書き込みでロールを分け、アプリケーションごとに認証情報を持たせ、適切な場面では行レベルセキュリティを使います。次のインタプリタのバグが、自動的にデータベース全体の流出を意味してはいけません。これは、ある層が破られたときに穏やかに劣化するシステムを作るという意味で、まさに防御的エンジニアリングの本道です。
  4. キルスイッチを実際に練習しておく。サービスを15分以内に静的なメンテナンスページへ切り替える、文書化されてテスト済みの手段を用意してください。その手順書を書くのは、開示の猶予期間中であってはなりません。
  5. ホストを使い捨てにする。不変イメージ、SSHでの職人芸の排除、認証情報のローテーションを日常業務にします。そうすれば「焼いて作り直す」は危機ではなく、ただのデプロイになります。

事前告知されたパッチの猶予期間は、あなたの緩和策がどれだけ巧妙かを試すものではありません。あなたの地味なインフラがどれだけ速く動けるかを試すものです。

今回の猶予期間が実際に測ったもの

ドラマを取り払えば、この事件はPHPのストレステストではなく、運用の成熟度のストレステストでした。インタプリタにはバグがあった。インタプリタにはいつもバグがあります。エコシステム全体で大きく差が出たのは、被害半径と対応時間でした。ある者は迷わず停止し、協調された時刻にパッチを当て、昼食前にはクリーンなイメージから再構築していました。別の者は、INI設定をいじりながら祈るように猶予期間を過ごしました。違いはセキュリティの洗練度ではなく、デプロイパイプライン、資産の棚卸し、そして長期的な侵害を避けるために短期的なダウンタイムを受け入れる意志でした。

ですから、冒頭の主張は但し書き付きで維持します。重大で事前告知されたインタプリタのバグが悪用されるまで数時間しかなく、あなたの緩和策が当て推量にすぎないなら、プラグを抜くのが大人の判断です。満足感はありませんし、デモ映えもしません。しかしセキュリティの態勢は、危機の最中にどれだけ忙しく見えるかで測られるものではありません。事後に「何も侵入していない」と証明できるかどうかで測られるのです。今週、動かしているすべてのPHPプロセスについてその問いに答えられないなら、それこそが直すべき本当のバグです。

インシデント解決後、緑のステータスモニターが並ぶ静かな管制室
最良のインシデント対応とは、緑のランプが灯り、驚きがないままに終わるものです。