WordPressの深刻度「緊急」脆弱性 wp2shell の概要と対応指針

2026年7月17日、認証不要でリモートからの任意コード実行(RCE)につながる脆弱性「CVE-2026-63030」「CVE-2026-60137」を修正したWordPress 6.8.6、6.9.5、および 7.0.2 が公開されました(参考:WordPress 7.0.2 Release)。Searchlight Cyber の Adam Kues 氏により報告された脆弱性です。

弊社 GMO Flatt Security リサーチチームの追加検証では、脆弱なバージョンでは標準構成でも、管理者の認証情報等を要さずに、リモートからの任意コード実行が可能である と判断しています。直近発見された脆弱性の中でも、相対的に深刻度が高く、WordPress のアップデートなしでは、WordPress データベースやサーバ内のさまざまな情報の窃取や、ページの改ざん、他悪性スクリプトの設置など、様々な被害が想定しうる状況です。

本稿では、これらの脆弱性の概要と、それらを組み合わせることで成立する未認証RCEの仕組みを概説します。また、利用者として取るべき対策・緩和策や、想像しうる侵害痕を整理します。

TL;DR

  • WordPress 6.9.0から6.9.4、7.0.0から7.0.1には、バッチルート混同のCVE-2026-63030とSQLインジェクションのCVE-2026-60137がある。
  • 両者の組み合わせによって、認証不要・外部からの任意コード実行(Pre-Auth RCE)が可能である。これが「wp2shell」と呼ばれている攻撃チェーンである。
  • 本稿公開時点では、フルチェーンのPoCは公開されておらず、公開されているPoCで任意コード実行するにはMySQLの設定不備等の条件が必要である。一方、弊社リサーチチームの調査、デフォルトセットアップでも任意コード実行が可能であることが確認できた。日本の三連休(7/18土〜7/20月)中において、アップデートなしでは、被害が甚大になる可能性もある。
  • WordPress 6.8.0から6.8.5はCVE-2026-60137の影響を受けるが、CVE-2026-63030の影響を受けない。従って、それらバージョンでは、攻撃チェーン「wp2shell」による Pre-Auth RCE が不可能である。
  • 根本対策は、7.0.2、6.9.5、6.8.6のうち、運用中のブランチに対応する修正版への更新となる。
  • 攻撃に使われるREST APIルートは/batch/v1で、標準的なURLは/wp-json/batch/v1?rest_route=/batch/v1の二つとなる。
  • 更新までの緩和策として、WAFで両方のURL形式を遮断するか、未認証利用者による/batch/v1の利用を禁止するべきである。

はじめに

本記事の目的は事態の把握と対応の促進であり、違法行為への加担・助長を意図するものではありません。 記述の一部には不正確な情報が含まれている可能性があります。 速報性を優先していますので、ご了承ください。

脆弱性の概要

2026年7月17日、以下の二つの脆弱性情報とパッチが公開されました。

これら二つを組みわせることで、外部から未認証のユーザ(WordPress サイトを閲覧しに来る不特定多数と同等の条件)で、WordPress アプリの任意のコードを実行できてしまいます。この脆弱性2つの攻撃チェーンは「wp2shell」と呼ばれています。

二つのCVEについて、WordPressのバージョンごとの影響と更新先を以下に示します。

WordPressのバージョン CVE-2026-60137 CVE-2026-63030 wp2shell RCE 推奨される更新先
6.8未満 対象外 対象外 対象外 最新の安定版を推奨
6.8.0から6.8.5 影響あり 対象外 対象外 6.8.6以上
6.9.0から6.9.4 影響あり 影響あり 影響あり 6.9.5以上
7.0.0から7.0.1 影響あり 影響あり 影響あり 7.0.2以上
7.1 Beta 1まで 影響あり 影響あり 影響あり 7.1 Beta 2以上

CVE-2026-60137(SQLインジェクション)の原因となるコードは、WordPress 6.8.0から存在していたようです。但し、6.8系の標準構成では、SQLインジェクションを引き起こすパラメータへの外部からの入力経路が無くプラグインやテーマの実装不備が追加で必要な状況でした。従って、少なくともデフォルトのセットアップにおいては、本脆弱性は攻撃不可能なものでした。

一方、WordPress 6.9以降のバッチ処理にて追加で発生した脆弱性CVE-2026-63030(REST APIのバッチルート混同)により、この脆弱性は攻撃可能なものとなりました。この脆弱性は、WordPress REST API の Batch API /batch/v1(複数の REST API へのリクエストを、一つの HTTP リクエストへのリクエストで、まとめて処理できるエンドポイント)の処理において発生したものです。本脆弱性を悪用すると、以下が可能になり、結果として任意のコードを実行できるようになります。

  • 本来 Batch API 経由では呼び出せない WordPress REST API エンドポイント(例:GET 系エンドポイント)を呼び出すこと。
  • ある REST API エンドポイントに対して、そのロジック起動前に行われるバリデーション(例:パラメータの存在確認、型チェック、権限チェック等)をバイパスすること。

脆弱性の仕組み

以下では、これら二つの脆弱性の悪用について、その原因について詳細に解説します。

CVE-2026-60137:author__not_in のSQLインジェクション

1つ目の脆弱性は、CVE-2026-60137(SQLインジェクション)です。

脆弱性が存在した場所と悪用可能性

CVE-2026-60137は、src/wp-includes/class-wp-query.php 内の WP_Query::get_posts()(名前の通り WordPress のポストを取得するための処理)に埋め込まれた脆弱性です。

具体的には、本関数の取るパラメータ author__not_in の処理に不備がありました。これは、検索結果から、除外する投稿者IDを指定するクエリ変数です。修正前の脆弱な処理を以下に引用します。

if ( ! empty( $query_vars['author__not_in'] ) ) {
    if ( is_array( $query_vars['author__not_in'] ) ) {
        $query_vars['author__not_in'] = array_unique(
            array_map( 'absint', $query_vars['author__not_in'] )
        );
        sort( $query_vars['author__not_in'] );
    }

    $author__not_in = implode(
        ',',
        (array) $query_vars['author__not_in']
    );
    $where .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) ";
}

値が配列の場合は、各要素をabsint()で整数へ変換していました。一方、スカラー値の場合はこの変換を通らず、(array) で配列へキャストされた後、implode()の結果がSQLのNOT IN句へ直接連結されていました。

このため、プラグインやテーマなどが信頼できないスカラー値を author__not_in へ渡すと、SSQL文の構造を変更される可能性がありました。

修正内容

修正版では、入力形式にかかわらず wp_parse_id_list() を通し、整数IDだけをSQLへ渡すよう変更されました。 修正差分の主要部分を以下に示します。

- if ( is_array( $query_vars['author__not_in'] ) ) {
-     $query_vars['author__not_in'] = array_unique(
-         array_map( 'absint', $query_vars['author__not_in'] )
-     );
-     sort( $query_vars['author__not_in'] );
- }
- $author__not_in = implode( ',', (array) $query_vars['author__not_in'] );
- $where .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) ";
+ $author__not_in_id_list = wp_parse_id_list(
+     $query_vars['author__not_in']
+ );
+ if ( count( $author__not_in_id_list ) > 0 ) {
+     sort( $author__not_in_id_list );
+     $where .= sprintf(
+         " AND {$wpdb->posts}.post_author NOT IN (%s) ",
+         implode( ',', $author__not_in_id_list )
+     );
+     $query_vars['author__not_in'] = $author__not_in_id_list;
+ }

wp_parse_id_list()は、配列と文字列のどちらを受け取った場合も、値を整数IDのリストへ正規化します。空のリストではNOT IN 句を生成せず、正規化後の値をクエリ変数へ戻すことで、キャッシュキーも同じ整数IDを基に生成されます(参考:修正コミット)。

CVE-2026-63030:REST APIのバッチルート混同

2つ目の脆弱性は、CVE-2026-63030(REST APIのバッチルート混同)です。

REST API のバッチルートとは

CVE-2026-63030は、src/wp-includes/rest-api/class-wp-rest-server.php 内の WP_REST_Server::serve_batch_request_v1() に埋め込まれた脆弱性です。本処理は以下の通り、REST API エンドポイント /batch/v1 から呼び出される処理です。

'/batch/v1' => array(
    'callback' => array( $this, 'serve_batch_request_v1' ),
    'methods'  => 'POST',
    'args'     => array(
        'requests' => array(
            'required' => true,
            'type'     => 'array',
            'maxItems' => $this->get_max_batch_size(),
        ),
    ),
),

エンドポイント /batch/v1 は、REST API エンドポイントへのリクエスト複数回を、1つの HTTP リクエストへまとめるエンドポイントです。例えばイメージとしては、以下のように用いることで、2つの記事の投稿を1つのリクエストで行うことができます。

POST /wp-json/batch/v1

{
  "requests": [
    {
      "method": "POST",
      "path": "/wp-json/wp/v2/posts",
      "body": {
        "title": "Article 1"
        "content": "Content of Article 1"
        "status": "draft"
      }
    },
    {
      "method": "POST",
      "path": "/wp-json/wp/v2/posts",
      "body": {
        "title": "Article 2"
        "content": "Content of Article 2"
        "status": "draft"
      }
    }
  ]
}

このエンドポイントは、?rest_route= クエリパラメータを使っても到達できます。 以降の解説では、概ね、以下2つのエンドポイントは同じものと考えてください。

POST /wp-json/batch/v1
POST /?rest_route=/batch/v1

脆弱性が存在した場所と悪用可能性

さて、バッチルートが、いかにして脆弱性を引き起こすのかを解説します。

まずエンドポイント /batch/v1 の中身である serve_batch_request_v1() は、バッチに含まれるサブリクエスト(便宜上の呼称。前記の例で言えば 2 つの POST /wp-json/wp/v2/posts リクエスト)ごとに、リクエスト本体、対応するルートとハンドラー、検証結果をそれぞれ $requests$matches$validation という 3 つの変数に保持していました。後続処理は、同じ添字を使って三つの配列から対応する要素を取り出し、サブリクエストを処理していきます。

勿論、これら 3 つの変数(配列)の長さは同一であることが期待されます。一方、サブリクエストの1つにバリデーションエラーが発生すると、この長さがズレる場合が出てしまう、という実装不備があるコミットで追加されました。これが CVE-2026-63030(REST APIのバッチルート混同)の原因です。

修正前の(=脆弱な) serve_batch_request_v1() のコードを、簡単のために書き直したものを以下に示します:

// ループ1: サブリクエスト1つずつに対して…
foreach ( $requests as $single_request ) {
    // サブリクエストがエラーの場合は、検証結果にエラーを記録してスキップ
    if ( is_wp_error( $single_request ) ) {
        $has_error    = true;
        $validation[] = $single_request;
        continue;
        // $matches には追加されていない・・・★
    }

    // このリクエストを処理するハンドラを取得
    $matches[] = $this->match_request_to_handler( $single_request );
    $validation[] = ...; // 実際には追加の検証処理がありますが、簡単のため省略
}

// ループ2: 改めて、サブリクエスト1つずつに対して…
foreach ( $requests as $i => $single_request ) {
    // サブリクエストがエラーの場合は、スキップ
    if ( is_wp_error( $single_request ) ) {
        continue;
    }

    // このサブリクエストを処理するハンドラを取得
    $match = $matches[ $i ];
    list( $route, $handler ) = $match;

    // このサブリクエストを実際に処理
    $this->respond_to_request( $single_request, $route, $handler, null );
}

重要なのは、コード内★の箇所の指摘通り、$matches が追加されない場合があるということです。 これが $requests=サブリクエストの列と、$matches =リクエストハンドラの列の間で、不整合を引き起こしてしまいます。

例えばバッチエンドポイントに対してサブリクエスト列 [S1, S2, S3] を送信し、このうちサブリクエスト S1 のみバリデーションエラーが発生した場合を考えると、以下のような状態になります。

$requests = [S1, S2, S3]
$validation = [エラー, 成功, 成功]
$matches = [S2, S3]

この時、ループ2でのサブリクエスト実行処理において、サブリクエスト S3 は、サブリクエスト S2 のハンドラを使って実行されてしまいます。結果として、攻撃者は、この挙動を用いて以下が引き起こせることになります:

  • ある REST API エンドポイントに対して、そのロジック起動前に行われるバリデーション(例:パラメータの存在確認、型チェック、権限チェック等)をバイパスすること。
  • 別の REST API エンドポイントに対して、そのロジック起動前に行われるバリデーションをバイパスすること。

これが CVE-2026-63030(REST APIのバッチルート混同)の主たる問題点です。このようなインデックスのズレは、コミット 880bb48にて導入され、6.9.0から6.9.4、7.0.0から7.0.1に適用されました。

修正内容

不正なサブリクエストについても$matchesへ同じWP_Errorを追加し、三つの配列の添字を維持するよう修正されました。修正差分を以下に示します。

foreach ( $requests as $single_request ) {
    if ( is_wp_error( $single_request ) ) {
        $has_error    = true;
+       $matches[]    = $single_request;
        $validation[] = $single_request;
        continue;
    }

この変更により、後続処理の$matches[$i]は常に$requests[$i]と同じサブリクエストを参照します(参考:修正コミット)。

同じリリースでは、REST APIの処理中に別の最上位REST処理を開始できないよう、再入防止のガードも追加されています。serve_request()に追加された処理を以下に示します。

public function serve_request( $path = null ) {
+   if ( $this->is_dispatching() ) {
+       return false;
+   }

    // 通常のRESTリクエスト処理
}

また rest_api_loaded()にも同じ状態を確認するガードが追加され、REST処理中の再入を二つの入口で拒否します(参考:修正コミット)。これも重要な修正で、RCE を回避するためのものです。詳細は割愛します。

脆弱性の悪用時に残る痕跡

ここまでの解説の範囲では、これら2つの脆弱性をチェーンした場合でも「SQL インジェクション可能なエンドポイントに外部から到達しうる」までは可能でも、それ以上が可能かは明らかではありません。また、本稿の公開時点では、脆弱性を悪用するコード(PoC 用 Exploit コード)は公開されていません。

一方、弊社 GMO Flatt Security リサーチチームの検証による、脆弱なバージョンでは標準構成でも、管理者の認証情報等を要さずに、リモートからの任意コード実行が可能であると判断しています。従って、本件は日本の三連休(7/18土〜7/20月)中に、広範囲に侵害が進行してしまう可能性もあります。

本セクションでは、これら検証結果のもと、脆弱性が悪用された場合に残る痕跡を示します。

アクセスログ(初期侵入痕)

攻撃チェーン「wp2shell」を外部からの成立させるためには、WordPress REST APIルート/batch/v1 を利用する必要があります。 WordPressの構成ごとの標準的なリクエスト先を以下に示します。

WordPressの構成 標準的なリクエスト先
パーマリンクが有効 POST /wp-json/batch/v1
rest_routeを使用 POST /?rest_route=/batch/v1
サブディレクトリに設置 POST /<設置先>/wp-json/batch/v1など

WordPressは、_methodクエリパラメータとX-HTTP-Method-OverrideヘッダーによるHTTPメソッドの上書きを処理します。 ログ検索を、受信時のHTTPメソッドがPOSTのリクエストだけに限定すると、上書きを使った通信を見落とす可能性があります(参考:WP_REST_Server::serve_request())。

従って、侵害有無の判定では、少なくとも下記パスへのリクエストログの確認が必要です。

  • POST /wp-json/batch/v1
  • POST /?rest_route=/batch/v1
  • POST /<設置先>/wp-json/batch/v1

アクセスログの例を以下に示します。

192.0.2.10 - - [18/Jul/2026:12:00:00 +0900] "POST /wp-json/batch/v1 HTTP/1.1" 207 684 "-" "example-client"
198.51.100.20 - - [18/Jul/2026:12:01:00 +0900] "POST /?rest_route=/batch/v1 HTTP/1.1" 403 153 "-" "example-client"

ただし残念ながら、攻撃が成功したか・失敗したかは、ログのみから確認できません。バッチAPIは、バッチ全体の応答に通常207 Multi-Statusを使い、個々のサブリクエストのステータスをJSON本文へ格納するためです。殆どのシナリオでは考えにくいことですが、万が一レスポンスログがボディまで残っている場合は、そのJSON本文を確認することで、攻撃の成否を判断できます。

検索結果では、URIだけでなく、_method=POST と、リクエストヘッダが記録されている場合は X-HTTP-Method-Override: POST も確認します。関連するログに記録される情報と、そのログだけでは判断できない点を以下に示します。

アクセスログ以外のログに残る痕跡(悪用痕)

バッチエンドポイントは初期侵入の入口に過ぎません。従って、仮に攻撃が成功した場合は、その後の任意コード実行のための操作にかかるログや、その後の攻撃活動における痕跡が種々のログに記録されます。

初期侵入以外で残る痕跡を本稿で明示すると、攻撃者に対して任意コード実行の筋道を指南することに繋がるため、この時点では本ブログでは詳細に開示いたしません(申し訳ございません・・・)。もし初期侵入があったと疑われる場合には、GMO Flatt Securityのお問い合わせ窓口にお問い合わせください。

GMO Flatt Security リサーチチームによる Pre-Auth RCE 検証の範囲で判明した、追加の侵害痕情報をご共有いたします。日本時間での三連休 7/18(土)〜7/20(月)中も、ベストエフォートで応対いたします

攻撃への根本対策

根本対策は、WordPress Coreを修正版へ更新することです。 7.0系は7.0.2以上、6.9系は6.9.5以上へ更新します。 6.8系はwp2shellのRCE対象ではありませんが、CVE-2026-60137を修正するため6.8.6以上へ更新します。

WAFやプラグインによるアクセス制限は脆弱なコードを修正しないため、更新の代わりにはなりません。 自動更新を利用している場合も、管理対象の全サイトが修正版になったことを確認してください。

攻撃への緩和策

wp2shellは、外部から /batch/v1 へ送られたリクエストを起点として成立します。そのため、WordPress Coreを更新するまでの一時的な緩和策として、バッチエンドポイントへの未認証アクセスを遮断することが望ましいといえます。

WAFでバッチエンドポイントを遮断する

WAFでは、次の二つを同じWordPress内部ルートとして扱い、両方を遮断します。

/wp-json/batch/v1
rest_route=/batch/v1

ただし、遮断においては、以下に気をつける必要があります。

  • URLデコードと正規化を行った後の値で判定する(小手先のバイパスを避けるため)。
  • 受信時のHTTPメソッドを限定せずに遮断する(_methodクエリパラメータ等のテクニックがあるため)。
  • WordPressをサブディレクトリに設置している場合は、そのパスも対象に含める。
  • オリジンサーバーへの直接接続を制限し、WAFを迂回できない構成にする。
  • 例外を設ける場合は、WAFで認証を検証できる利用者か、厳密に管理した送信元だけを許可する。

Cloudflareで配備されている関連ルールを以下に示します(参考:配備情報)。

対象 Cloudflare Managed Ruleset Cloudflare Free Ruleset
CVE-2026-60137 1c060d3a371549219ee290d7ed933fcc db003b39b7774859a8d588ce33697a1a
CVE-2026-63030 7dfb2bd4708d4b88b9911dc0550664b6 ebd3f2df15c74ddcbf6220c9b5ec246a

該当ルールを無効化する例外や、BlockをLogへ変更するオーバーライドがないことも確認します。

プラグインで未認証アクセスを禁止する

WAFを利用できない場合は、rest_pre_dispatchフィルターで/batch/v1への未認証アクセスを拒否できます。バッチエンドポイントだけをHTTP 401で拒否するプラグイン実装例が、wp2shell 公式サイト で公開されているため、参照ください。

終わりに

本稿は、日本国内において WordPress が多く採用されていること、及び脆弱なバージョンを利用している場合に攻撃の成立条件が著しく緩い(ほぼ無条件に再現する)ことを加味して、広く脆弱性の詳細と対応策を届けることを目的に執筆したものです。

もし被害が疑われる場合、または被害の有無が判定できずお困りの場合は GMO Flatt Securityのお問い合わせ窓口にお問い合わせください。日本時間での三連休 7/18(土)〜7/20(月)中も、ベストエフォートで応対いたします。GMO Flatt Security リサーチチームによる Pre-Auth RCE 検証の範囲で判明した、侵害調査における追加情報や、追加のご支援をご共有いたします。(勿論フォレンジックやインシデント初動対応等、恐縮ながら費用を要する範囲のサービスもある点はご了承願いたいものの、あくまでブログ等公開媒体での開示は望ましくないと判断した情報は無償提供させていただきます。)

謝辞

攻撃チェーン「wp2shell」は、AssetnoteおよびSearchlight CyberのAdam Kues氏によって発見され、WordPressへ報告されました(参考:発見者による公開情報)。発見や、プラグイン例含む早期対策の案内に、敬意を評します。

WordPressの深刻度「緊急」脆弱性 wp2shell の概要と対応指針

プロフィール
注目記事