緊急PHPに致命的な脆弱性(CVSS 9.2)。Creemaと札幌市も予防的にサイトを停止最新情報 ›
無料で相談する

php2shell(CVE-2026-109618/109619)技術解説|修正コードから読む2つのバグと、SAPI別の影響・緩和策の根拠・検知

公開:10/22 01:10更新 (日本時間)Webセキュリティ診断センター 編集部約26分で読めます
php2shell(CVE-2026-109618/109619)技術解説|修正コードから読む2つのバグと、SAPI別の影響・緩和策の根拠・検知
#php2shell#PHP#脆弱性#技術解説

この記事は技術者向けです。一般の方は、影響と対策を平易にまとめた「php2shell の解説記事」をご覧ください。

この記事の要点
  • 2件とも、修正は1つのコミット「Avoid state corruption via bailout」にまとまっています。中身は「メモリ不足(OOM)のときに longjmp で処理を抜けるのをやめる」ことと、「リクエストの境目でグローバル状態をリセットし直す」ことの2つです。
  • CVE-2026-109618 は、初期化の途中で OOM が起きると、後片付けが中途半端なデータを読んでしまう問題です。mod_php はリクエストの開始処理に失敗しても後片付けを必ず呼ぶため、アプリのコードが動く前に成立します。
  • CVE-2026-109619 は、解放済みのメモリを指すグローバル状態が残り、同じワーカーの次のリクエストが通常の初期化より前にそれを使う問題です。確認された経路は、PHP 8.5 の致命的エラーのバックトレースと、Session Upload Progress です。
  • Apache・PHP-FPM の SAPI のソースは変わっていません。SAPI ごとの差は、開始処理に失敗したときの振る舞いと、リクエストの前に INI を当てる経路の有無から生まれています。

この記事では、攻撃の再現手順や攻撃コードには触れません。PHP 公式の GitHub セキュリティアドバイザリ(GHSA)と、php-src の修正コミットを読み、修正の意図と、防御側が判断するための材料に絞って書きます。推測を含む箇所は、そのつど「推測」と書いています。

概要(CVE・CVSS・影響範囲・クレジット)

PHP 公式は 2026年10月21日、2件を修正した PHP 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 を公開しました。GHSA の公開時刻は 13:34 UTC(日本時間 22:34)です[php.net・GitHub]。

項目CVE-2026-109618CVE-2026-109619
GHSAGHSA-w8hw-w9fx-j79wGHSA-6wmf-4rv5-p3rp
題名RCE via OOM corruptionRCE via stale global state
深刻度Critical。CVSS v4.0 で 9.2。ベクターは2件とも CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
CWE2件とも同じ。CWE-416(解放後の使用)、CWE-755、CWE-824、CWE-1325、CWE-1419
影響バージョン(GHSA の記載)8.2.35 未満、8.3.36 未満、8.4.27 未満、8.5.12 未満。GHSA に載っているのはサポート中のブランチだけで、8.1 以前の範囲は書かれていない
修正版8.2.35 / 8.3.36 / 8.4.27 / 8.5.12。7.4・8.0・8.1 は、ソースのみの最小限のバックポート
確認された影響Web ワーカーの権限での、認証なしの RCE。mod_php で完全な RCE を確認認証なしのヒープの解放後使用、メモリの漏えい、RCE。セッション経路の RCE は mod_php と FPM で確認
報告者(GitHub アカウント)lcfr-hax、sagitzadik-wiz、tomash-areion、amyb-oai
修正の開発者arnaud-lb、iluuu1994、bukka、ndossche(コミットの共同作者には Nicolas Grekas も入っている)

CVSS の AC:H(攻撃の難しさが高い)と AT:P(攻撃に前提条件がある)は、GHSA の記載と照らすと、ヒープの配置への依存や「同じワーカーにリクエストが届く必要がある」といった条件を反映したものと読めます(推測。GHSA はベクターの各値の理由を書いていません)。GHSA は「ワーカーが多いと1回あたりの成功率は下がるが、条件はなくならない」としています。

発見の経緯については、Wiz が自社ブログで、8月19日に最初の脆弱性を見つけ、8月24日に PHP のセキュリティチームへ報告し、9月21日に PHP 側が受領して修正の調整が始まったと説明しています。調査には Wiz Atlas、Gemini 4 Argon、GPT‑5.6 Sol を使ったとしています。Cloudflare は、脆弱性を開示したのは Calif.io、Wiz、OpenAI、Quarkslab だと書いています[Wiz・Cloudflare]。

Wiz のブログと GHSA の対応づけについて
Wiz のブログには、upload progress まわりの解放後使用やメモリの漏えいを CVE-2026-109618 側に、バックトレースや OPcache の問題を CVE-2026-109619 側に並べているように読める表があります。一方、GHSA では Session Upload Progress もバックトレースも CVE-2026-109619 の「確認された経路」として書かれています。この記事では GHSA の記載に従います。

修正コミットの全体像

サポート中のブランチの修正は、PHP-8.2 ブランチの1コミットです。これが 8.3 → 8.4 → 8.5 へ順にマージされています。

ブランチコミット内容
PHP-8.23584a98Avoid state corruption via bailout(作成日 2026-10-18)。続く a34a205 で NEWS に 8.2.35 の項目を追加
PHP-8.3c1a5f46PHP-8.2 からのマージ(2026-10-21 14:12 UTC)
PHP-8.48a94bb3PHP-8.3 からのマージ(同 14:27 UTC)
PHP-8.5c7273b9PHP-8.4 からのマージ(同 14:30 UTC)

コミットメッセージは次の2行で、そのあとに2つの GHSA が並んでいます。

- Replace bailout on OOM with UnwindExit
- Properly reset various globals that may be accessed during startup

PHP-8.2 では 389 ファイル、10,759 行の追加と 988 行の削除です。追加の多くはテスト(oom_*.phpt など)です。本体で変わったのは、Zend エンジン(Zend/zend_alloc.c のメモリ管理、zend_execute_API.c、zend_hash.c、VM など)、main/(main.c、SAPI.c、rfc1867.c、php_variables.c、ストリーム)、そして ext/session、ext/standard、ext/opcache、ext/pcre、ext/json、ext/mbstring など多数の拡張です。

一方、sapi/apache2handler と sapi/fpm のソースは変わっていません(FPM はテスト1件のみ)。sapi/ 以下で変わったソースは fuzzer と phpdbg だけです。SAPI ごとの影響の違いは、SAPI を直したのではなく、エンジン側の直し方で吸収されています。

同じワーカーが処理するリクエスト1とリクエスト2の流れ。リクエスト1の開始処理でOOMが起きると後片付けが中途半端なデータを読む(CVE-2026-109618)。リクエスト1で残ったグローバル状態を、リクエスト2の通常の初期化より前の処理が使う(CVE-2026-109619)
図1 リクエスト処理の流れと、2件の問題が起きる位置

CVE-2026-109618:OOM で中断した後の後片付け

GHSA の説明

PHP は、メモリ不足(OOM)のエラーで longjmp() を使い、いまの処理から一気に抜けてリクエストの処理を打ち切ります(PHP の中では bailout と呼びます)。GHSA によると、リクエスト用のメモリが、初期化を終える前に一時的に参照できる状態になる箇所があります。そこでメモリ確保に失敗すると、途中で抜けたことが考慮されず、メモリが不正な状態のまま残ります。失敗した確保は、その不正なメモリと無関係でもかまいません。そのあとリクエストの終了処理が、その状態を読んだり、その値で分岐したりします。

GHSA は、PHP の上限の範囲内に見えるリクエストで OOM に届く要因として、multipart の解析について次の3点を挙げています。

このほか、アプリの処理で大きなメモリ確保が起きる場合も OOM に届くとしています。

修正 1:OOM で longjmp しない

修正前の Zend/zend_alloc.c では、上限を超えると zend_mm_safe_error() が E_ERROR を出したうえで bailout していました(3584a98 で削除された行)。

-	heap->overflow = 1;
-	zend_try {
-		zend_error_noreturn(E_ERROR, format, limit, ... size);
-	} zend_catch {
-	}  zend_end_try();
-	heap->overflow = 0;
-	zend_bailout();

修正後は、memory_limit を超えても、その場では抜けません。新しい zend_mm_check_limit() が、エラーの内容を EG(delayed_oom) に記録し、VM への割り込みフラグ(EG(vm_interrupt))を立てるだけです。呼び出し元は確保に成功したものとして処理を続けます(後述の fallible な確保では NULL が返ります)。記録された OOM は、VM の安全な位置(命令と命令の間)で zend_handle_delayed_oom() が処理し、スクリプトの実行中なら UnwindExit を投げて、通常の例外と同じ経路で巻き戻します。GHSA はこれを「次の VM の安全な位置で発動する、遅延した穏やかな soft OOM」と説明しています。

ただし、それだけだとメモリを使い放題になります。そこで2つ目の上限として hard limit が入りました。初期値は memory_limit の 200%(memory_limit が 1GB を超える場合は +1GB)で、新しい INI 項目 memory_hard_limit_percent(100 以上、または -1)で変えられます。hard limit に届くと、次のメッセージを標準エラー出力に書き、_exit(137) でプロセスごと終了します。

Hard memory limit of %zu bytes exhausted (tried to allocate %zu bytes) in %s on line %d

mmap() などの下位の確保そのものに失敗した場合も、修正後は bailout せず、Out of memory を出して exit(1) します。プロセスを終わらせるので、中途半端な状態を後片付けが読むことはありません。

修正 2:multipart の解析を、OOM を前提に書き直す

OOM ですぐ抜けなくなったので、解析の処理は「OOM が記録されたら、自分で片付けて抜ける」必要があります。main/rfc1867.c では、メモリを確保したあとに ZEND_HAS_OOM() を確かめ、確保済みのバッファを解放して fileupload_done へ抜ける分岐が数十か所に入りました。本文の読み込みなど一部の確保は、失敗すると NULL を返す fallible な確保(safe_ferealloc(..., /* fallible */ true))に変わっています。GHSA が挙げた要因には、次の変更が対応していると読めます。

空のファイル欄と max_file_uploads の関係については、この記事を書いた時点で、対応する変更箇所を特定できていません。

+	if (SG(post_max_size) > 0 && SG(read_post_bytes) > SG(post_max_size)) {
+		return 0;
+	}

(main/rfc1867.c の fill_buffer()。3584a98)

開始処理での本文の解析が終わったところでは、main/SAPI.c の sapi_handle_post() が記録された OOM を処理します。解析の関数がバッファを片付けて戻ったあとに、エラーとして扱う順序です。拡張(ext/pcre、ext/standard/string.c、ext/json、Zend/zend_hash.c など)の多数の変更は、OOM で抜けなくなったことに合わせて、確保の失敗や ZEND_HAS_OOM() を確かめる処理を足したものと見られます(推測。全ファイルは読み切れていません)。

CVE-2026-109619:リクエストをまたいで残る状態

GHSA の説明

PHP のワーカーには、エラー、セッション、オブジェクト、拡張などのグローバル状態があり、その一部は1つのリクエストが持つメモリを指しています。本来は、リクエストのメモリを解放する前にリセットする必要があります。ところが、入力、設定、セッション、エラー処理の一部は、次のリクエストの通常の初期化より前に動きます。そこで前のリクエストがリセットし損ねた状態を読み、解放後の使用になります。

GHSA は、成立には「書き手(producer)」と「読み手(consumer)」の両方が要るとしています。書き手は、リクエストのメモリを指す値をグローバルな場所に置く処理です。致命的エラー(OOM、構文エラー、require の失敗、Maximum execution time exceeded など)でリセットの前に処理が打ち切られると、値が残ります。終了処理の後半で出た警告が、いったん消したエラー状態を書き戻すこともあります。読み手は、同じワーカーの次のリクエストで、通常の初期化より前にその値を使う処理です。確認された読み手は次の2つです。

具体例として、GHSA は2つを「確認済み」としています。1つは PHP 8.5 の fatal_error_backtraces で、致命的エラーのバックトレースの状態が終了処理で必ずリセットし損ねられていたものです。mod_php での RCE はこの経路で確認されています。もう1つはセッションの upload progress で、OOM などで処理が打ち切られると、セッションやデコーダーのグローバル状態が解放済みのメモリを指したまま残ります。

修正:境目でのリセットを足す

修正は、残りうる状態を1つずつリセットする形です。主なものを挙げます(いずれも 3584a98)。

+	/* Runs before RINIT: no user code may be invoked, see php_session_unserialize_init() */
+	PS(in_rfc1867_update) = true;
+	if (php_session_initialize() == FAILURE || PS(session_status) != php_session_active) {

(ext/session/session.c の php_session_rfc1867_update())

GHSA は、修正では確認された経路以外の状態もまとめてリセットしたとしています。どの状態が実際に悪用できるかは、アプリ・拡張・状態が置かれた位置しだいだという説明です。

SAPI 別の考え方

PHP 公式は、Web のリクエストを処理するすべての PHP(PHP-FPM、mod_php、php-cgi、FrankenPHP、LiteSpeed)を対象とし、mod_php のリスクが最も高いとしています[php.net]。差が出る理由は主に2つです。1つは、開始処理(php_request_startup())に失敗したときに、後片付けを呼ぶのか、プロセスを終えるのか。もう1つは、通常の初期化より前に INI を当てる経路があるかどうかです。

SAPI109618(OOM)109619(残留状態)
Apache mod_php開始処理に失敗しても終了処理を必ず呼ぶ。本文の自動解析で OOM を起こせば、アプリのコードより前に成立する。既定の入力・メモリ上限と通常の prefork 構成で完全な RCE を確認.htaccess と Apache の php_value・php_flag・php_admin_* が php_request_startup() より前に当たり、INI の読み手になる。バックトレース経路とセッション経路の両方で RCE を確認
PHP-FPM開始処理に失敗すると子プロセスを終えるので、不正な状態は使われない。ただし開始処理が成功したあとの OOM は残る。ソースの分析からは影響あり、FPM での完全な攻撃は未実証nginx などが PHP_VALUE/PHP_ADMIN_VALUE を送り、それが初期化前に警告を出す場合に条件付きで影響。プール設定はワーカー起動時、.user.ini は初期化後に当たるので、読み手にならない。アプリの実行中に状態が置かれ、致命的エラーのあと upload progress が読む経路で、通常の dynamic プールでの RCE を確認(適した処理がアプリにあることが前提)
FrankenPHPソースの分析では mod_php と同じく終了処理を必ず呼ぶため、アプリに依存しない攻撃を受ける可能性が高い既定の設定では、初期化前の INI の読み手は確認されていない。ただし影響がないという意味ではない
LiteSpeed(LSAPI)FPM と同じ作り。アプリ次第の攻撃は可能性が高いが未検証環境変数で INI を早い段階に当てられるため、条件付きの候補
php-cgiGHSA に個別の記載はない。php.net は対象に含めている

(表の内容は、php-cgi の行を除いて GHSA の記載です)

mod_php の振る舞いは、変更されていない sapi/apache2handler/sapi_apache2.c で確かめられます。php_apache_request_ctor()(中で php_request_startup() を呼ぶ)が失敗すると zend_bailout() し、zend_end_try() を抜けたあと、無条件に php_apache_request_dtor()(php_request_shutdown())を呼びます。PHP-FPM の sapi/fpm/fpm/fpm_main.c は、php_request_startup() == FAILURE のとき php_module_shutdown() してプロセスを終えます。

php-cgi について補足します。ここからは推測です。リクエストごとにプロセスを起動する従来の CGI であれば、ワーカーが次のリクエストを処理しないので、109619 の「同じワーカーの次のリクエスト」という条件は満たしにくいと考えられます。FastCGI として常駐させ、1プロセスで複数のリクエストを処理する使い方では、FPM と同じように考えるのが安全です。どちらの使い方でも、109618 の影響は残ると考えてください。

公式の緩和策が効く理由・効かない理由

緩和策の一覧と副作用は「php.ini の設定変更や WAF で防げる?」にまとめています。ここでは、上で見た仕組みから、なぜ効くのか・どこが残るのかを整理します。どの策も、PHP 公式は「完全な対策は更新だけ」としています。

公式の緩和策ごとに、CVE-2026-109618とCVE-2026-109619のどちらに効くかをまとめた表。修正版への更新は両方に効く。memory_limit=-1とpost_max_sizeは109618の一部、upload_progressの無効化とfatal_error_backtraces=0と古いINI指定の除去は109619の一部、upload_progressをini_setや.user.iniで無効化しても効かない
図2 公式の緩和策が効く範囲

memory_limit=-1(109618)

確認された攻撃は、攻撃者が狙って起こせる「memory_limit 超過」の OOM を使います。上限をなくせば、その OOM は起きません。それでも mmap() などの下位の確保が失敗すれば、修正前の PHP は同じ longjmp() の経路に入ります。GHSA が「完全な緩和ではない」とするのはこのためです。RLIMIT_AS や RLIMIT_DATA で代用してはいけない理由も同じで、上限に届くと mmap() が失敗し、脆弱な経路を自分から呼び込むことになります。cgroup の memory.max は代わりになりえますが、確保の失敗を防ぐ保証はないとされています。

アプリから上書きされない形(FPM プールや Apache の php_admin_value)で設定するよう求めているのは、どこかで有限の値に戻されると、制御できる OOM が復活するからです。

post_max_size と前段での上限(109618)

GHSA が挙げた OOM の要因(Content-Length のない本文、ヘッダーの数、入れ子)は、どれも「宣言された大きさより多くのメモリを使わせる」ものです。前段(Web サーバーやプロキシ)で、デコード後の大きさ、項目数、パート数、ヘッダー数、入れ子の深さに上限をかければ、PHP に届く前に断れます。GHSA は、Content-Length がないリクエストにも上限がかかるようにすること、multipart を使わないアプリなら前段で断ることを求めています。修正版を当てたあとも、hard OOM(プロセスの終了)とサービス停止のリスクを下げるとしています。

session.upload_progress.enabled=0(109619)

確認されたセッション経路では、upload progress が書き手にも読み手にもなります。無効にすると、両方がなくなります。ほかの経路(バックトレースや INI の警告など)には効きません。

ini_set() や FPM の .user.ini で設定しても効かないのは、適用のタイミングの問題です。upload progress は、本文の解析中、つまりスクリプトが動く前に動きます。ini_set() はスクリプトの中で呼ぶものなので間に合いません。GHSA によると .user.ini も通常の初期化のあとに当たります。そのため php.ini、FPM プール、本文の解析より前に当たるサーバー設定で入れ、全ワーカーを再起動する必要があります。GHSA は、修正版を当てたあとも、この機能を使わないなら無効のままにしておくのが賢明だとしています。

fatal_error_backtraces=0(PHP 8.5・109619)

確認された書き手のうち、バックトレースの状態をなくします。修正では、この状態を終了処理でリセットするようになりました。その修正がない状態で、書き手そのものを止める策です。

INI の警告をなくす・pm.max_requests=1(109619)

読み手の1つは、初期化前に当たる INI の警告です。.htaccess や PHP_VALUE に、非推奨の項目(auto_detect_line_endings=1、明示した mbstring.internal_encoding など)や不正な値が残っていると、それが読み手になります。GHSA は、設定を php.ini・FPM プール・起動時のサーバー設定に寄せ、古い指定を消すよう勧めています。

性能を問わないなら pm.max_requests=1 も挙げられています。1ワーカーが1リクエストしか処理しなければ、「同じワーカーの次のリクエスト」が来ないためです。

検知:ログとプロセスで見るところ

GHSA が挙げるログの確認

GHSA(CVE-2026-109619)は、隔離した環境での監査として、起動時の設定で log_errors=On、使える error_log、error_reporting=E_ALL にし、Apache・PHP-FPM・リバースプロキシ・サービスのログで次を探すよう勧めています。

同時に GHSA は、ログにエラーがないことは安全の証明にならないとしています。過去のログは非推奨などを除いて集めていたかもしれず、E_ALL でも、試したルートと状態しか分かりません。メッセージを書く前に失敗が起きることもあります。

修正前の環境で気にしたいログ

ここからは、GHSA の説明とコードからの推測を含みます。

修正後の環境で出るようになるログ

これらは修正が働いた結果であり、攻撃の証拠とは限りません。ただ、普段は出ないメッセージが特定の URL に集中していれば、試行を疑う手がかりになります(推測)。

プロセスとレスポンスの観点

確認された影響は「Web ワーカーのアカウント権限での RCE」です。侵入後の痕跡として、Web サーバーや PHP のプロセスがシェルやダウンロードツールを起動していないか、Web ルートに知らないファイルが増えていないか、を確かめます。手順は「Webサイトが乗っ取られていないか確かめる方法」にまとめています。

Cloudflare は、同じ HTTP トランザクションのリクエストとレスポンスをあわせて調べる「full transaction detection」を新しく用意し、php2shell 向けの検知がこれを使う最初のマネージドルールだと説明しています。レスポンス側のルールは、条件に合うオリジンの応答を、要求元に届く前に止められます。リクエストだけでは攻撃と正規の通信を見分けきれないことがある、というのが理由です。マネージドルールセット(無料・有料)に「Anomalous Execution」1〜7 を、全プラン向けのレスポンス側ルールに1件を追加し、無料・有料の WAF の利用者は 10月21日 03:02 UTC から保護されていたとしています。攻撃者がルールの回避を試みていることも観測したとしています[Cloudflare]。

バックポートとディストリビューションの番号

7.4・8.0・8.1 向けの最小限のバックポート

PHP のセキュリティチームは、サポートが終わった 7.4・8.0・8.1 にも、ソースだけの修正ブランチを出しました。コミットメッセージは「Minimal mitigation for EOL version.」です。

ブランチコミット
PHP-7.4-CVE-2026-109618-109619712e60f
PHP-8.0-CVE-2026-109618-10961982f8f31
PHP-8.1-CVE-2026-109618-1096191d7dec1

中身はサポート中のブランチと大きく違い、変更は 7.4 で 17 ファイル・約 240 行の追加です。

このため、自分でビルドして使う場合は「upload progress が使えなくなる」「メモリ上限に届いたリクエストはワーカーごと落ちる」ことを前提にしてください。php.net は、8.1 以前はディストリビューションや LTS の事業者のバックポートを確かめるか、サポート中のバージョンへ移るよう求めています[php.net]。

ディストリビューションの番号

php.net によると、ディストリビューションと LTS の事業者には事前にパッチが渡されていました。番号の決め方はディストリごとに違います。

Ubuntu・Debian での具体的な番号の確かめ方と更新の手順は「VPS・自社サーバーのPHPを修正版に更新する手順」にまとめています。更新後は、PHP-FPM か Apache を再起動しないと古いワーカーが残る点にも注意してください。

修正版の適用や設定の見直しを、Webセキュリティ診断センターに頼むこともできます
SAPI と設定を確かめたうえでの修正版の適用、緩和策の設定、更新後のログ・侵害の痕跡の確認までお手伝いします。無料相談はこちら

参考にした情報

貴社のサイトに合った対応をご案内します!
まずは、お気軽にご相談ください。

お問い合わせ・お見積りはこちら
無料で相談する最新情報を見る