WordPress 7.1.2 で直った脆弱性 CVE-2026-87902 と、いま確認すること

WordPress 7.1.2 セキュリティリリース CVE-2026-87902 の解説記事のアイキャッチ WordPress
この記事は約12分で読めます。

WordPress 7.1.2 では、何が直ったのでしょうか。
修正は1件だけです。ログインしていない攻撃者が、ページテンプレートを探す処理を使って、テーマの外にある PHP ファイルを読み込ませられる脆弱性(CVE-2026-87902)でした。サーバーとテーマの条件がそろうと、任意のコード実行まで届きます。

リリースは2026年9月22日で、対象は WordPress 4.7 から 7.1.1 まで。約10年分の版がまとめて当てはまります。
自動更新が効いているサイトなら、もう 7.1.2 になっているはずです。その「はず」が引っかかったので、確認の手順もあわせて書いておきます。

get_page_template() のテンプレート候補の流れ。修正前はページテンプレート指定の分岐だけに validate_file() があり、pagename をデコードする分岐には検査がなかった。7.1.2 ではその分岐に validate_file() が加わり、最後に _wp_is_template_path_allowed() でテーマ内のパスかを確かめる

CVE-2026-87902 の影響範囲。WordPress 4.7 から 7.1.1 までが対象で、7.1 系は 7.1.2、7.0 系は 7.0.6、6.9 系は 6.9.9、6.8 系は 6.8.10、6.7 系は 6.7.9 など、4.7 系の 4.7.37 まで各系統に修正版が出ている。4.6 以前には修正版がない

3行上にあった検査が、ここにはありませんでした

問題の場所は wp-includes/template.php の get_page_template() です。固定ページを表示するとき、この関数は page-{スラッグ}.php のようなテンプレート候補の一覧を作り、テンプレートローダーに渡します。

候補の作り方は2通りあります。ページテンプレートの指定から作る候補は、WordPress 自身のパス検査である validate_file() を通してから一覧に入れていました。
一方、リクエストから来る pagename をもとに作る候補は、この検査を通っていませんでした。しかも urldecode() で一度デコードしてから候補にする分岐があり、ここで上位ディレクトリをたどる記述が本物のパスとして効いてしまったようです。
Patchstack の解説は、守りは3行上に書かれていたのに、隣の分岐にだけ抜けていた、と指摘しています。コードを並べて見ると、抜けが一目で分かります。

ただ、どのサイトでもすぐ乗っ取られる、という話ではありません。
ファイル名は page- で始まり .php で終わる形に組み立てられます。そのため Patchstack によると、有効なテーマの直下に page-templates のような page- で始まるディレクトリがあることが実質的な前提になるそうです。古いデフォルトテーマや、人気のあるサードパーティ製テーマの一部がこの形をしているとのことでした。

読み込ませたファイルから任意のコード実行へ進むには、サーバー側の条件も要ります。Patchstack は PHP の register_argc_argv が有効な環境を挙げていて、公式の PHP Docker イメージや、PHP 8.5 未満の cPanel 環境では既定で有効だと書いています。珍しい設定ではない、というのが彼らの見立てです。
認証なしでファイルを読み込ませるところまでは常に届き、コード実行はサーバー次第。CVSS 4.0 で 9.2 という数字は、この組み合わせから来ています。

この脆弱性を、もう少し分解してみます

ここまでで「検査のある分岐と無い分岐が隣り合っていた」という要点は見えました。ここからは、攻撃者の側から見て、この穴がどう「使える」形になっていたのかを順を追って見ていきます。
分類上は CWE-98(include/require に渡すファイル名の不適切な制御)で、CVSS 4.0 のスコアは 9.2(Critical)、ベクトルは AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N です。報告者は Robert Ressl 氏で、アドバイザリ GHSA-7hp8-65ch-5whp として2026年9月22日に公開されました。

まず、なぜ「認証なし」で届くのか

入口になる pagename は、リクエストから素直に入ってくるクエリ変数です。ログインは要りません。
ただし、pagename 単体を投げても WordPress は 404 を返すだけで、テンプレート解決の処理までたどり着きません。固定ページとして解決できる状態を作らないと、問題の get_page_template() が呼ばれないからです。そのため実際の攻撃リクエストでは、公開済みの固定ページを指す page_id が pagename と一緒に付いています。この2つが同時に並ぶことは通常のアクセスではまず無いので、あとで触れる検知の手がかりにもなります。

次に、なぜ「エンコードされた ../ 」でないと通らないのか

ここが、この脆弱性のいちばん技巧的なところだと思いました。
WordPress はスラッグをテンプレート候補にする前に、独自のサニタイズを通します。この処理は、素の .(ドット)を書き換えたり、素の /(スラッシュ)で切り詰めたりする一方で、パーセントエンコードされたオクテット(%2e や %2f)はそのまま残します。
つまり、生の ../../ はサニタイズで潰れて通りません。ところがエンコードした %2e%2e%2f は生き残り、そのあと get_page_template() の中の urldecode() が呼ばれた瞬間に、本物の ../ に戻ってしまう。サニタイズを一度すり抜けてから、デコードで牙が生える、という二段構えです。

組み立てられるファイル名は page-{デコード後の値}.php の形でした。先頭が page- で固定され、末尾に .php が付く。
だから攻撃者は、まず page- で始まる実在のディレクトリ(典型的には page-templates)の中に一度入り、そこから ../ で登って目的のファイルへ向かう、という経路を取らざるを得ません。これが本文で触れた「有効なテーマの直下に page- で始まるディレクトリがあること」が前提になる理由です。ディレクトリの深さに合わせて、攻撃側は ../ の段数を1段から12段くらいまで振ってきます。

そして、なぜ「ファイル読み込み」が「コード実行」に化けるのか

ここは条件付きです。.php ファイルを読み込ませても、実行されるのはそのファイルが持っている処理であって、攻撃者が書いた任意のコードそのものではありません。
鍵になるのが PEAR の pearcmd.php です。このファイルは、PHP の register_argc_argv が有効なとき、クエリ文字列を $argv(コマンドライン引数)として受け取ってしまう性質があります。
そこに config-create のような命令を渡すと、pearcmd は指定された内容・指定された場所にファイルを書き出します。攻撃者が内容と場所を決められる PHP ファイルが書ける、というのは、実質的にコード実行です。読み込ませる先が pearcmd.php になった時点で、LFI が RCE に化けます。

register_argc_argv は珍しい設定ではありません。公式の PHP Docker イメージや、PHP 8.5 未満の cPanel 環境では既定で有効です。
「認証なしのファイル読み込みは常に成立し、そこからコード実行まで届くかはサーバー次第」——CVSS 9.2 という数字は、この組み合わせの怖さを表しています。

直し方を見て、そこまでやるのかと思いました

7.1.2 の修正は2つあります。1つ目は抜けていた検査を足すもので、デコード後の値にも validate_file() をかけるようになりました。

2つ目は、新しく入った _wp_is_template_path_allowed() という内部関数です。どの経路で決まったテンプレートでも、最終的なパスが子テーマか親テーマのディレクトリ、または theme-compat の中にあるかを確かめます。
報告された穴を塞ぐだけなら1つ目で足ります。Patchstack はここから、セキュリティチームがテンプレート解決を1件のバグではなく、同じ種類の問題の集まりとして扱ったと読んでいます。私も同じ読みです。

変更されたファイルは wp-includes/template.php の1つだけで、同梱パッケージの更新はありません(公式のリリースノートより)。

寄り道をひとつ。Patchstack は9月18日付で、7.1.1 で修正された RCE の記事も出しています。短い間隔でセキュリティリリースが続いたので、7.1.1 の手動更新を済ませて安心していた人ほど、今回を見落としやすいかもしれません。

自分のサイトで確かめるなら、ここからだと思います

まず版数です。管理画面の「更新」画面でも見られますが、WP-CLI が使えるなら次の2行で済みます。

7.1.2 と出れば、この件は片付いています。
7.1 に上げずに 7.0 系で運用しているサイトなら、7.0.6 になっていれば修正済みです。最新版でなくても、修正の入った版はあります。6.9 系は 6.9.9、6.8 系は 6.8.10、6.7 系は 6.7.9 と、4.7 系の 4.7.37 まで各系統に修正版が出ていて、全部の番号は公式のリリースノートに並んでいます。
ただし、公式に保守されているのは最新版だけです。旧系統の修正版は、あくまで厚意で出されたものです。

次に、自動更新が本当に動いているかです。WordPress はマイナーリリースとセキュリティリリースを既定で自動適用しますが、wp-config.php の定数や、プラグイン、ホスティング側の設定で止まっていることがあります。

定数が定義されていなければエラーが返るだけで、それ自体は問題ありません。WP_AUTO_UPDATE_CORE が false、または AUTOMATIC_UPDATER_DISABLED が true と返ってきたら、コアの自動更新は止まっています。
管理画面の「ツール」→「サイトヘルス」にも、バックグラウンド更新が動くかどうかの検査項目があります。

【実例:raplsworks.com で確認した結果(7.1.2 に上がっていたか、自動更新の設定はどうだったか)をここに】

クライアントの事情などで、すぐには上げられないサイトもあると思います。その場合に Patchstack が勧めている確認が2つあります。修正の代わりにはなりませんが、最悪のケースにどれだけ近いかは分かります。
1つは、有効なテーマ(子テーマなら親テーマも)の直下に page- で始まるディレクトリがあるかどうかです。

もう1つは register_argc_argv の値です。ここには落とし穴があって、コマンドラインで php -i を見ても当てになりません。CLI 版の PHP では、この設定が常に有効として扱われるからです。
Web サーバー側の値を見るには、推測されにくい名前で次のようなファイルを一時的に置いてブラウザで開き、見終わったらすぐ消します。レンタルサーバーによっては、管理パネルの php.ini 設定画面で確認できる場合もあります。

攻撃は3つの段階に分かれていました

CVE-2026-87902 の悪用3段階。第1段階はwp-links-opml.phpなど無害なコアファイルを読み込ませて脆弱性の有無を確認、第2段階はpearcmd.phpに+config-showを渡してregister_argc_argvの有効性を確認、第3段階は+config-createで/tmpや/var/tmpにPHPファイルを書き込むコード実行。第2段階以降は少数のIPに絞られる
この CVE がやっかいなのは、パッチ公開と攻撃開始がほぼ同時だったことです。Patchstack のファイアウォールに最初の攻撃が届いたのは、7.1.2 が公開された当日、2026年9月22日 11:49(UTC)でした。パッチの差分を見て組み立てた、と分かる内容だったそうです。
観測された通信は、次の3段階にきれいに分かれていました。

第1段階(偵察):wp-links-opml.php や feed-rss2.php のような無害なコアファイルを読み込ませ、その応答で「このサイトは刺さるか」を確かめる段階です。攻撃の大半はいまもここです。
第2段階(到達確認):読み込み先を pearcmd.php に向け、クエリに +config-show を付けて、PEAR が存在し register_argc_argv の仕掛けが効くかを確かめます。コード実行の直前の一手です。
第3段階(書き込み):+config-create に切り替え、/tmp や /var/tmp に PHP ファイルを書き出します。ここまで来ると、そのホストは侵害されたものとして扱うべきです。

公開翌日の9月23日には、通信量が初日の10倍以上に跳ね上がり、専用の Nuclei テンプレート(スキャン自動化ツール)まで出回りました。
第1段階・第2段階はスキャン集団による広く浅い偵察ですが、第3段階の書き込みは、その結果を受けた少数のIPに絞られる——スキャンで作った「刺さるサイト一覧」を、別の攻撃者が刈り取る、という典型的な形です。
米国の CISA は9月25日にこの CVE を KEV(悪用が確認された脆弱性カタログ)へ追加し、連邦民間機関に対して9月28日までの対処を求めました。実害が出ている、という公的なお墨付きです。

すでに攻撃を受けていなかったか、ログで確かめる

本文の確認手順は「これから守る」ための話でした。ここでは「すでに刺されていないか」を振り返るための指標を挙げておきます。パッチ適用や緩和より前に公開されていた期間があるサイトは、その期間のアクセスログを見ておくと安心です。

アクセスログ側で手がかりになるのは、次のようなパターンです。

とくに注意したいのが、応答コードと中身の食い違いです。
ふつうの固定ページの URL なのに、応答が 200 で、しかも中身が OPML や RSS フィードだった——これは第1段階の偵察が「成功」した証拠です。テンプレート解決がテーマの外へ出て、コアファイルを読み込んで返してしまった、ということだからです。ブロックされたのではなく、通ってしまった痕跡として扱ってください。

サーバー側では、/tmp と /var/tmp に見覚えのない .php ファイルが無いかを確認します。もしあれば、第3段階の書き込みが成功したということで、そのホストは「スキャンされた」ではなく「侵害された」として対応する必要があります。

すぐに更新できない事情がある場合の緩和策も、本文の2つの確認に加えて2つあります。1つは、pagename にトラバーサル(../ やそのエンコード)を含むリクエストを WAF で弾くこと。正規の固定ページのスラッグにこの文字列は入らないので、通常のアクセスを巻き込まずに止められます。
もう1つは register_argc_argv を無効化すること。これは読み込み自体は塞ぎませんが、pearcmd の連鎖を断ち切れます。情報漏えいで留まるか、コード実行まで許すかの分かれ目がここです。
どちらも本修正の代わりにはなりません。最終的な答えは、あくまで 7.1.2(または各系統の修正版)への更新です。

プラグインを書く側としても、他人事ではありませんでした

今回の穴は、検査を書かなかったことより、検査のある分岐と無い分岐が隣り合っていたことにあります。
同じ値を加工してから使う経路が増えると、元の値は守っていても、加工後の値が素通りになる。urldecode() を挟んだ分岐がまさにそれでした。

ファイルパスを組み立てるプラグインでは、入口で一度だけ検査して終わりにしない。include や require の直前で、最終的なパスが想定したディレクトリの中にあるかを確かめる。7.1.2 の2つ目の修正は、その形をコア自身がやってみせたものだと受け取っています。

ちなみに、修正版が出た一番古い系統は 4.7 です。4.7 のリリースは2016年12月なので、ほぼ10年前の版まで手当てされたことになります。
4.6 以前の版には、今回の修正版は出ていません。

WordPress
この記事を書いた人
rapls

WordPressのプラグインを作っているフリーランスエンジニアです。Web開発はもう6年以上。WordPress.orgで Rapls AI Chatbot、Thanks Mail for Stripe、Rapls PDF Image Creator、Prime Cache の4本を公開し、保守を続けながら、日本語ロケールの翻訳エディター(PTE)も務めています。このブログに書くのは、現場で自分が実際にハマって、調べて、直した話です。

raplsをフォローする
raplsをフォローする

コメント

タイトルとURLをコピーしました