2026年10月21日、PHPに対して2つのCritical(CVSS 9.2)な脆弱性のアドバイザリが公開されました。
- GHSA-w8hw-w9fx-j79w (CVE-2026-109618)
- GHSA-6wmf-4rv5-p3rp (CVE-2026-109619)
どちらも認証不要でトリガーできるUse-After-Free(解放済みメモリの使用)で、最終的には任意コード実行(RCE)まで到達することが実際に確認されています。この記事では、2つの脆弱性がどういう仕組みで発生し、なぜUAFからRCEまで繋がるのかを、整理します。
対象バージョンと対応
- 影響を受けるバージョン: PHP 8.2.35未満 / 8.3.36未満 / 8.4.27未満 / 8.5.12未満
- 修正バージョン: 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12
対応は「PHPを修正バージョンに更新する」以外に確実な方法はありません。緩和策(memory_limit=-1の強制、session.upload_progress.enabled=0など)はいくつか案内されていますが、どれも「攻撃の難易度を上げる」程度のもので、脆弱性そのものを塞ぐものではない点に注意してください。
全体像: 受付係のたとえ
技術的な話に入る前に、全体像をたとえ話で押さえておきます。
PHPのワーカープロセスは、お客さん(リクエスト)が来るたびに部屋を片付けて、次のお客さんを迎える受付係のようなものです。
- **1つ目の脆弱性(CVE-2026-109618)**は、片付けの途中で「このお客さんの荷物が多すぎて片付けられない」とパニックになり、部屋がぐちゃぐちゃのまま次の処理に進んでしまう問題です。攻撃者はメモリを使い切らせるような大きいリクエストを送って、このパニックを意図的に起こします。
- **2つ目の脆弱性(CVE-2026-109619)**は、前のお客さんのメモ書きが机の上に残ったまま片付けが中断されるパターンです。次のお客さんが来たとき、本来は新しいメモに差し替わるはずなのに前の人のメモがまだ残っていて、それを読んでしまう。しかもそのメモが指している場所はもう取り壊された部屋なので、読みにいくと異常が起きます。
ここから、それぞれを技術的に見ていきます。
脆弱性1: CVE-2026-109618 — OOM時のUse-After-Free
仕組み
PHPはメモリ不足(OOM)を検知すると、longjmp()というC言語の仕組みを使って現在の処理を強制的に中断し、エラーハンドリングのポイントまで一気に抜け出していました。
問題は、リクエストメモリがまだ完全に初期化されていない、一時的にアクセス可能な状態でこの割り当て失敗が起きた場合です。longjmp()で処理を飛び越えてしまうと、本来実行されるはずだった初期化の後始末がされず、不完全な状態のメモリ構造が残ります。この後に実行される後片付け(shutdown処理)が、この不完全な状態を「正常なメモリ構造」として扱ってしまうことで、未初期化のポインタ参照やUse-After-Freeが発生します。
攻撃者はどうやってOOMを起こすのか
攻撃者はmultipart/form-data形式のリクエストを使って、意図的にメモリを使い切らせることができます。具体的には以下のようなテクニックが確認されています。
- Content-Lengthを省略してサイズチェック(
post_max_size)を素通りさせる - 1つのパートに無制限の数のヘッダーを詰め込み、通信量(ワイヤ上のサイズ)より実質的に大きなメモリを消費させる
- 空のファイル項目を送ることで
max_file_uploadsの上限はすり抜けつつ、通常のファイルメタデータ配列は生成させる
SAPI(実行環境)ごとの危険度の違い
同じ脆弱性でも、どのSAPI(PHPの実行方式)で動いているかによって危険度が変わります。
- Apache mod_php / FrankenPHP: リクエスト起動処理が失敗していても無条件にshutdown処理を呼び出してしまうため、自動ボディパース中にメモリ上限を超えると、不完全な状態のまま後片付けが走ります。デフォルト設定の下で完全なリモートコード実行がHTTP経由で実証されています。
- PHP-FPM: 起動処理(request startup)が失敗した場合は、その子プロセスごと終了させる仕組みがあるため、同じ経路はそのままでは成立しません。ただし、起動が正常に終わった後でメモリ不足に陥るケースは理論上あり得るとされており、「完全なPoCはまだ無いが、安全とは言い切れない」という位置づけです。
修正内容
PHP 8.4.27以降では、OOM検知時のlongjmp()そのものを廃止し、
- 次のVMセーフポイントで安全に中断する「soft OOM」
- メモリ消費が第二閾値に達した場合に
_exit()で即座に終了する「hard OOM」
という2段構えの仕組みに置き換えられました。これにより、初期化が不完全な状態で処理を飛び越えてしまうこと自体がなくなります。
脆弱性2: CVE-2026-109619 — ワーカー再利用時のダングリングポインタ
仕組み
mod_phpやFPMのワーカープロセスは、1つのプロセスで複数のリクエストを順番に処理します。この構造の中で、PHPはエラー情報やセッション状態などを「ワーカーが保持するグローバル変数」に一時的に格納しますが、その中身はリクエストごとに確保された(リクエスト終了時に解放される)メモリを指しています。
通常はリクエスト終了時にこのグローバル変数が正しくリセットされますが、以下のような致命的エラーが発生すると、このリセット処理がスキップされることがあります。
- パース・コンパイルの失敗
- キャッチされない例外
requireの失敗- 実行時間制限(
max_execution_time)の超過 - メモリ枯渇によるOOM
リセットがスキップされたまま、そのリクエストのメモリ(ヒープ)は解放されます。しかしワーカーが持つグローバル変数は、もう存在しない領域を指すダングリングポインタとして残り続けます。そして同じワーカーが次のリクエストを処理する際、通常の初期化処理が走る前にこの古い状態を読んでしまうコードパスがあることが、この脆弱性の核心です。
確認されている2つの悪用経路
fatal_error_backtraces(PHP 8.5の機能): 有効にしていると、致命的エラー発生時のバックトレース情報がリクエスト所有のメモリに保存されますが、このリセットはshutdown時に常に漏れるとされています。session.upload_progress: アップロード処理中にセッション状態が初期化されますが、これはデフォルトで有効で、アプリケーション側でsession_start()を呼んでいなくても到達します。致命的エラーでリセットが飛んだ後、次のアップロード進捗の操作が古いセッションポインタに触れてしまいます。
重要なのは、error_reportingの変更や@演算子の利用、display_errors/log_errorsの無効化では、この脆弱性を防げないという点です。これらはエラーの「表示」を制御するだけで、内部状態のリセット漏れそのものには関与しません。
SAPIごとの実証状況
- mod_php: 上記2つの経路いずれでも、完全なRCEが実証されています。
- PHP-FPM: FPMは起動処理が失敗した場合に子プロセスを終了させるため、mod_phpと全く同じ経路(起動時のOOM)は成立しません。ただし、アプリケーション実行中に致命的エラーが発生し、次のリクエストで
session.upload_progressがその古い状態を消費する、という経路でRCEが実証されています。「FPMだから安全」とは言えません。
修正内容と緩和策
修正版では、リクエストスコープのグローバル状態を確実にリセットするパッチが適用されています。更新できない場合の緩和策としては、
fatal_error_backtraces=0session.upload_progress.enabled=0pm.max_requests=1でワーカーの寿命そのものを短くする(パフォーマンスを犠牲にする代わりに、再利用そのものを減らす)
などが案内されていますが、いずれも対症療法であり、根本的な解決にはなりません。
なぜUAFがRCEまで到達するのか: ヒープグルーミング
ここまでの説明は「解放済みのメモリを読んでしまう」という話でしたが、読むだけならクラッシュか情報漏洩で済むはずです。ここからRCEまで繋がるのには、もう一段階の仕組みが必要です。
PHPのメモリアロケーターは、一度解放された領域を次の確保要求があったときに再利用します。つまり攻撃者は、脆弱なコードが解放済み領域を読みにいく直前のタイミングを狙って、同じサイズの別のデータを確保させ、同じ領域を自分の用意したデータで上書きすることができます。これを**ヒープグルーミング(ヒープスプレー)**と呼びます。
解放された領域には元々、PHP内部の構造体の一部──例えばオブジェクトのクラス情報へのポインタや、関数ポインタ、配列の長さといった情報が入っていました。攻撃者がそこに偽のデータを流し込むと、「このポインタの先にはクラス定義がある」と信じて読みに行ったコードが、実は攻撃者が用意した偽のクラス定義や偽の関数ポインタを読んでしまいます。
その結果、PHPが内部で「この関数を呼ぶ」と判断した瞬間に、実際には攻撃者が指定したアドレスへジャンプする、という制御フローの乗っ取りが起きます。ここまで来れば、最終的にsystem()相当の処理を呼び出すなど、任意のコードを実行させることが可能になります。
両アドバイザリとも、具体的なペイロードの内容までは公開されていませんが、「ヒープレイアウトの精密な制御が鍵である」と明記されており、このグルーミングの精度が攻撃の再現性を左右していると考えられます。
まとめ
| CVE-2026-109618 | CVE-2026-109619 | |
|---|---|---|
| 種類 | OOM時のUse-After-Free | ワーカー再利用時のダングリングポインタ(UAF) |
| 根本原因 | longjmp()による不完全な状態での処理中断 | 致命的エラーによるグローバル状態のリセット漏れ |
| トリガー | multipart/form-dataでメモリを使い切らせる | 致命的エラー + session.upload_progressなど |
| mod_php | 完全なRCE実証済み | 完全なRCE実証済み |
| PHP-FPM | 起動失敗時は守られるが、起動後の経路は理論上残る(未実証) | 実行中の致命的エラー経由でRCE実証済み |
| 修正版 | 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 | 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 |
いずれも認証不要・CVSS 9.2のCriticalな脆弱性であり、PHP-FPMであっても「対象外」とは言えません。緩和策はいくつかありますが対症療法に過ぎないため、PHP 8.4.27以降への更新を優先する必要があります。