当社のAIエンジンNeutronが、UCバークレーのCyberGymベンチマークで97.1%のスコアを記録しました。 詳細を見る

ディープダイブ

ディープダイブ

LatePoint 予約プラグインのアカウント乗っ取り(CVE-2026-118066)

LatePoint の未認証 SQL インジェクションにより、攻撃者が顧客アカウントを乗っ取れます。5.7.7 で修正され、5.7.8 で強化が完了します。

要点

  • 未認証の攻撃者が、文字列 IS NOT NULL をパスワードリセット用トークンとして送信し、LatePoint の顧客アカウントのパスワードをリセットできます。Wordfence がこの脆弱性を報告し、CVE レコードは Jakub Herman をクレジットし、2026年11月7日に公開されました。
  • LatePoint 5.7.6 以前が影響を受けます。このプラグインには 100,000 件の有効なインストールがあります。脆弱性は、センチネルを SQL に変換するクエリビルダーにあり、2 つの公開 AJAX ルートから到達できます。
  • CVE レコードは CVSS v3.1 9.1 の Critical と評価しています。実際の環境での悪用は報告されておらず、2026年11月7日時点で CVE は CISA の KEV カタログにありません。
  • 5.7.8 に更新してください。更新できない場合は、文書化された 2 つのリクエストパターンをブロックし、顧客のパスワードリセットを監査してください。詳しくは「修正方法」を参照してください。

影響を受けますか?

次の条件がすべて当てはまる場合にのみ影響を受けます。

  • サイトが LatePoint プラグイン 5.7.6 以前を実行している。
  • プラグインが有効で、公開 AJAX ルートがリクエストに応答する。
  • account_nonse が NULL でない顧客レコードが少なくとも 1 件ある。LatePoint は顧客にこの値を作成するため、予約が 1 件あるサイトでも該当します。
  • 攻撃者が顧客のメールアドレスを入手できる。アドバイザリの攻撃チェーンは注文インテントから 1 件読み取ります。公開された予約確認やスタッフ名簿から入手できる場合もあります。
  • WordPress アカウントへの影響については、wp_users_as_customers オプションが有効である。

脆弱なコードはプラグインの中核となる顧客クラスとクエリクラスにあります。オプションのモジュールは不要です。2 つのルートは未ログインの訪問者向けに登録されているため、攻撃者はサイトのアカウントを必要としません。

ブランチ 影響を受けるバージョン 修正バージョン
LatePoint 5.7.6 以前 5.7.7(2026年11月2日)、モデルの完全な修正は 5.7.8(2026年11月3日)

CVE レコードは 5.7.6 以前を影響範囲として挙げています。5.7.7 は文書化された 2 つのリクエスト経路をブロックします。5.7.8 はクエリビルダーの文字列リテラル分岐を削除するため、5.7.8 を使用してください。

WP-CLI でインストール済みバージョンを確認します。

wp plugin get latepoint --field=version

WP-CLI がない場合は、プラグインのヘッダーを読み取ります。

grep -m1 "Version:" wp-content/plugins/latepoint/latepoint.php

5.7.6 までのバージョンが影響を受けます。

修正方法

  1. WordPress 管理画面から LatePoint を 5.7.8 に更新するか、wp plugin update latepoint を実行します。プラグインページは 5.7.8 を 2026年11月3日公開の最新版として示しています。
  2. wp plugin get latepoint --field=version で新しいバージョンを確認します。
  3. サイトがプラグインファイルのキャッシュ済みコピーを実行している場合は、キャッシュを消去し、プラグインヘッダーが 5.7.8 を示すことを確認します。
  4. 更新前の期間について、顧客のパスワードリセットとサインインを確認します。確認手順は「侵害されたかどうかを確認するには?」にあります。

更新できない場合は、更新できるまで LatePoint を無効化してください。プラグインを無効化すると予約ページと顧客用ページがなくなるため、メンテナンス時間を計画してください。

Web アプリケーションファイアウォールのルールで露出を減らせますが、コードは修正されません。ブロックする 2 つのリクエストパターンは次のとおりです。

  • route_name=customer_cabinet__change_password で、password_reset_token の値に IS NOT NULL が含まれるもの。
  • route_name=steps__start_from_order_intent で、order_intent_key の値に IS NOT NULL が含まれるもの。

URL デコード後のパラメーター値を大文字と小文字を区別せずに照合します。攻撃者はエンコードを変えられるため、文字列 IS+NOT+NULL だけに一致するルールでは他の形式を見逃します。実際のトークンに空白が含まれる場合にのみ正当なリセットをブロックしますが、プラグインの 16 進トークンに空白はありません。

ベンダーは回避策を挙げていません。顧客用ページに到達できるアカウントを制限しても、ルートは設計上公開されているため役に立ちません。

根本原因は何ですか?

クエリビルダーは、WHERE の値が文字列リテラル IS NOT NULL と等しい場合、バインドする値ではなく SQL キーワードとして扱います。LatePoint の OsModel::build_conditions_query() は各値を文字列と比較し、キーワードを SQL テキストに追加します。この分岐にはプレースホルダーもバインド値も追加しません。

5.7.6 の lib/models/model.php にある脆弱な分岐です。

} elseif ( $value === 'IS NULL' ) {
    // IS NULL
    $temp_query = $this->with_table_name( $column ) . ' IS NULL ';
} elseif ( $value === 'IS NOT NULL' ) {
    // IS NOT NULL
    $temp_query = $this->with_table_name( $column ) . ' IS NOT NULL ';
} elseif ( is_array( $value ) && ! empty( $value ) ) {
    $temp_query = $this->with_table_name( $column ) . ' IN ' . OsModel::where_in_array_to_string( $value );
} else {
    // Add to list of query values
    $where_values[] = $value;
}

顧客ヘルパーはリクエストの値をそのままこのビルダーに渡します。

public static function get_by_account_nonse( $account_nonse ) {
    if ( empty( $account_nonse ) ) {
        return false;
    }
    $account_nonse = sanitize_text_field( $account_nonse );
    $customer      = new OsCustomerModel();

    return $customer->where( [ 'account_nonse' => $account_nonse ] )->set_limit( 1 )->get_results_as_models();
}

sanitize_text_field() はタグと余分な空白を削除します。空白と SQL キーワードは保持するため、IS NOT NULL はそのまま通過します。ヘルパーは条件が account_nonse IS NOT NULL のクエリを構築します。account_nonse が NULL でない顧客すべてが一致し、set_limit(1) が 1 行を返します。

パスワードリセットのエンドポイントは、このヘルパーを認証チェックとして使用します。

} elseif ( ! empty( $params['password_reset_token'] ) ) {
    $params['password_reset_token'] = sanitize_text_field( $params['password_reset_token'] );
    $customer                       = OsCustomerHelper::get_by_account_nonse( $params['password_reset_token'] );

    // Validate token expiration (15 minutes)
    if ( $customer ) {
        $created_at = OsMetaHelper::get_customer_meta_by_key( 'password_reset_token_created_at', $customer->id );

エンドポイントは返された顧客をリセットトークンの所有者として扱います。トークンを保存済みの値と比較しません。残る唯一のチェックは 15 分間の鮮度タイムスタンプで、攻撃者は対象アドレスのリセットを要求して開始できます。

同じパターンが注文インテントのルートにもあります。OsOrderIntentHelper::get_order_intent_by_intent_key() は intent_key を where() に渡すため、params[order_intent_key]=IS NOT NULL は intent_key IS NOT NULL になり、最初の注文インテントを返します。予約フォームはそのレコードの顧客名とメールアドレスを表示します。

5.7.5 は別の検索、OsStepsHelper::set_customer_object() の顧客 uuid に is_scalar() チェックを追加しました。このチェックはネストした配列を拒否しますが、スカラー文字列 IS NOT NULL は受け入れるため、この経路は閉じません。

どのように悪用されますか?

  1. 攻撃者は顧客のメールアドレスを入手します。アドバイザリの攻撃チェーンは params[order_intent_key]=IS NOT NULL を公開ルート steps__start_from_order_intent に送信します。クエリは最初の注文インテントを返し、予約フォームはそのレコードの顧客名とメールアドレスを表示します。
  2. 攻撃者は公開ルート customer_cabinet__request_password_reset_token に params[password_reset_email]=<email> を付けて、そのアドレスのリセットを要求します。LatePoint は顧客にトークンをメールで送り、password_reset_token_created_at を保存します。15 分間のウィンドウが始まります。
  3. 攻撃者は公開ルート customer_cabinet__change_password に params[password_reset_token]=IS NOT NULL と新しいパスワードを付けて POST します。エンドポイントはセンチネルで顧客を検索し、最初に一致した行を返します。アドバイザリはこれを最小 ID の顧客と説明しています。
  4. エンドポイントはその顧客のリセットタイムスタンプを読み取ります。リセット要求によりタイムスタンプは新しく、15 分間のチェックに合格します。エンドポイントは攻撃者のパスワードで update_password() を呼び出します。
  5. 攻撃者は顧客としてサインインします。アカウントは顧客の予約、注文、個人データを公開します。wp_users_as_customers が有効な場合、同じ顧客レコードを通じてリンクされた WordPress アカウントに到達できます。

アドバイザリはリクエストパラメーターを文書化しています。アドバイザリから適応したパスワード変更リクエストの形式は次のとおりです。

POST /wp-admin/admin-ajax.php
Content-Type: application/x-www-form-urlencoded

action=latepoint_route_call&route_name=customer_cabinet__change_password&params[password_reset_token]=IS+NOT+NULL&params[password]=<new-password>&params[password_confirmation]=<new-password>

注文インテントのリクエストは、route_name=steps__start_from_order_intent と params[order_intent_key]=IS+NOT+NULL を使用する同じ形式です。

攻撃者はメールで送信されたトークンを必要としません。センチネルがトークン検索を置き換え、リセット要求は鮮度タイマーを開始するだけです。

攻撃の流れは?

未認証の攻撃者がリクエスト文字列を SQL 条件に変え、最初に一致した顧客を乗っ取り、エンドポイントが確認するリセットタイマーを開始します。

flowchart LR A["未認証のリクエスト"] --> B["password_reset_token に IS NOT NULL を設定"] B --> C["sanitize_text_field が文字列を保持"] C --> D["build_conditions_query が account_nonse IS NOT NULL を生成"] D --> E["set_limit(1) が最初の顧客を返す"] E --> F["新しいリセットタイムスタンプが 15 分間のチェックに合格"] F --> G["攻撃者のパスワードが顧客アカウントに書き込まれる"]

攻撃者に何ができますか?

未認証の攻撃者は、リクエストごとに LatePoint の顧客アカウントを 1 件乗っ取り、その顧客の予約、注文、個人データを読み取りまたは変更できます。上書きされるアカウントは、センチネルのクエリが返す最初の行で、アドバイザリはこれを最小 ID の顧客と説明しています。

影響は次の条件に依存します。

  • サイトが LatePoint 5.7.6 以前を実行し、プラグインが有効である。
  • 攻撃者が公開 AJAX ルートに到達できる。
  • 顧客レコードの account_nonse が NULL でない。
  • 攻撃者が顧客のメールアドレスを入手し、そのアドレスのリセットを要求できる。
  • パスワード変更リクエストが届いたとき、顧客のリセットタイムスタンプが 15 分間のウィンドウ内にある。
  • リンクされた WordPress アカウントについては、wp_users_as_customers が有効である。

CVE レコードは機密性と完全性を高、可用性をなしと評価しています。攻撃者は顧客データを読み取り、パスワードを変更できますが、脆弱性はコードを実行せず、セキュリティスコープも変更しません。プラグインは WordPress アカウントを顧客向けの安全なロールにのみリンクするため、サイトがその設定を変更していない限り、リンクされたアカウントは管理者ではなく購読者または顧客です。

アドバイザリは WordPress 管理者アカウントへの経路を示していません。顧客アカウントの乗っ取りを説明し、オプションが有効な場合はリンクされた WordPress の購読者または顧客アカウントも含みます。

修正が機能する理由

修正は、リクエスト文字列が SQL キーワードの分岐に到達するのを防ぎます。LatePoint 5.7.7 は文書化された 2 つの入口に厳密な文字チェックを追加します。LatePoint 5.7.8 はクエリビルダーから文字列リテラル IS NOT NULL の分岐を削除するため、内部のマーカーオブジェクトだけがキーワードを要求できます。

lib/controllers/customer_cabinet_controller.php の 5.7.7 のガードです。

             } elseif ( ! empty( $params['password_reset_token'] ) ) {
                 $params['password_reset_token'] = sanitize_text_field( $params['password_reset_token'] );
-                $customer                       = OsCustomerHelper::get_by_account_nonse( $params['password_reset_token'] );
+                // Reset token is a hex key - a value with spaces/operators (e.g. "IS NOT NULL") is never valid.
+                if ( is_string( $params['password_reset_token'] ) && ctype_alnum( $params['password_reset_token'] ) ) {
+                    $customer = OsCustomerHelper::get_by_account_nonse( $params['password_reset_token'] );
+                }

lib/controllers/steps_controller.php の 5.7.7 のガードです。

         public function start_from_order_intent() {
-            $order_intent = OsOrderIntentHelper::get_order_intent_by_intent_key( $this->params['order_intent_key'] );
+            // Intent key is a hex token - a value with spaces/operators (e.g. "IS NOT NULL") is never valid.
+            $order_intent_key = sanitize_text_field( $this->params['order_intent_key'] ?? '' );
+            $order_intent     = ( is_string( $order_intent_key ) && ctype_alnum( $order_intent_key ) ) ? OsOrderIntentHelper::get_order_intent_by_intent_key( $order_intent_key ) : new OsOrderIntentModel();

ctype_alnum() は IS NOT NULL の空白を拒否するため、検索はスキップされ、リクエストは安全に失敗します。同じリリースはマーカーオブジェクト null_condition() と not_null_condition()、およびヘルパー where_null() と where_not_null() を追加し、内部コードがリクエスト文字列なしで NULL 比較を表現できるようにします。

lib/models/model.php の 5.7.8 の変更は文字列リテラルの分岐を削除します。

-                    } elseif ( $value === 'IS NULL' || $value === self::null_condition() ) {
+                    } elseif ( $value === self::null_condition() || ( $value === 'IS NULL' && self::is_legacy_null_condition() ) ) {
                         // IS NULL
                         $temp_query = $this->with_table_name( $column ) . ' IS NULL ';
-                    } elseif ( $value === 'IS NOT NULL' || $value === self::not_null_condition() ) {
+                    } elseif ( $value === self::not_null_condition() ) {
                         // IS NOT NULL
                         $temp_query = $this->with_table_name( $column ) . ' IS NOT NULL ';
                     } elseif ( is_array( $value ) && ! empty( $value ) ) {

5.7.8 以降、文字列 IS NOT NULL はバインド値の分岐に落ちます。IS NULL リテラルは古い LatePoint Pro アドオン用にのみ残り、アドオンのバージョンが 1.7.7 より前の場合に限られます。CVE レコードは 5.7.7 を修正版として扱います。5.7.8 の変更は、リクエストデータを where() に渡す他の呼び出し元のために、モデルレベルの経路を閉じます。

侵害されたかどうかを確認するには?

まず、リクエスト本文を記録するログを確認します。Web アプリケーションファイアウォールの監査ログやリバースプロキシのリクエストログなどです。LatePoint はルートとパラメータを POST 本文から読み取るため、標準のアクセスログには admin-ajax.php へのリクエストだけが記録されます。攻撃者がパラメータをクエリ文字列で送信した場合、アクセスログに含まれます。

grep -Ei 'route_name=(customer_cabinet__change_password|steps__start_from_order_intent)' /var/log/apache2/*access*.log /var/log/nginx/*access*.log /var/log/modsec_audit.log 2>/dev/null

一致した行でセンチネルを URL デコード形式かつ大文字と小文字を区別せずに検索します。値は IS+NOT+NULL、IS%20NOT%20NULL、または混在したエンコードで現れることがあります。一致は手がかりであり、証拠ではありません。スキャナーも同じ文字列を送信できます。

予期しない顧客のリセットタイムスタンプを確認します。

wp db query "SELECT object_id, meta_value FROM wp_latepoint_customer_meta WHERE meta_key='password_reset_token_created_at' ORDER BY meta_value DESC LIMIT 50;"

wp_ をテーブル接頭辞に置き換えてください。LatePoint がリセットメールを送信すると、リセットタイムスタンプが書き込まれます。行をサポート記録と比較してください。顧客が要求していないリセットの後に、見慣れないアドレスからのサインインが続く場合は、より強い兆候です。

LatePoint は顧客のパスワード変更の監査ログを保持しません。顧客のパスワード変更報告をリセット行と比較し、同じ期間の顧客用ページへのサインインについて Web ログを確認します。

ベンダーは侵害の指標を公開していません。失敗した試行はベンダー固有の痕跡を残さず、成功した試行は変更されたパスワードとリセットタイムスタンプだけを残す場合があります。

年表

  • 2026年10月27日:CVE-2026-118066 が予約されます。
  • 2026年11月2日:LatePoint 5.7.7 が 2 つの入口のガードとマーカーヘルパーとともにリリースされます。
  • 2026年11月3日:LatePoint 5.7.8 がリリースされ、クエリビルダーから文字列リテラル IS NOT NULL の分岐を削除します。
  • 2026年11月7日:CVE レコードが 11:26 UTC に公開され、アドバイザリ GHSA-rmmm-8g4j-3qgc が 12:31 UTC に公開されます。

2 つの修正リリースは 2 日以内に出て、CVE レコードは 5.7.8 の 4 日後に続きました。
図 1:5.7.7 の修正から 5.7.8 と CVE レコードまで。出典:LatePoint の変更履歴と CVE レコード、2026年11月7日確認

よくある質問

攻撃者はサイトのアカウントを必要としますか?

いいえ。2 つのルートは未ログインの訪問者向けに登録されており、パスワード変更エンドポイントはリセットトークンを受け取ると nonce チェックを省略します。攻撃者にはサイトへのネットワークアクセスだけが必要です。

5.7.7 で十分ですか?

文書化された 2 つのリクエスト経路をブロックします。5.7.7 のクエリビルダーは依然として文字列リテラル IS NOT NULL を SQL として扱うため、リクエストデータを where() に渡す別の呼び出し元は分岐に到達できます。5.7.8 はその分岐を削除します。5.7.8 に更新してください。

攻撃者は WordPress 管理者アカウントを乗っ取れますか?

既定ではいいえ。この脆弱性は LatePoint の顧客レコードを乗っ取ります。プラグインは wp_users_as_customers が有効な場合にのみ顧客を WordPress アカウントにリンクし、そのリンクを顧客向けの安全なロールに制限します。ロールの割り当てを変更したサイトは設定を確認してください。

実際の環境で悪用されましたか?

2026年11月7日時点で実際の環境での悪用は報告されておらず、CVE は CISA の KEV カタログにありません。アドバイザリはリクエストパラメーターを含むため、手法は公開されています。

すぐに更新できない場合はどうすればよいですか?

更新できるまで LatePoint を無効化してください。予約フローをオンラインに保つ必要がある場合は、Web アプリケーションファイアウォールで 2 つのリクエストパターンをブロックし、リセットタイムスタンプを毎日監査します。ファイアウォールルールは露出を減らしますが、クエリビルダーは修正しません。

失敗したリクエストはサイトが安全であることを示しますか?

いいえ。センチネルは最初に一致した顧客を返すため、攻撃者がリセットタイマー用のメールアドレスを間違えてもリクエストは成功する場合があります。失敗したパスワード変更でも、顧客に一致し、鮮度チェックだけで失敗した可能性があります。ログとリセット行を合わせて確認してください。

参考資料

  • GitHub アドバイザリ GHSA-rmmm-8g4j-3qgc、2026年11月7日公開、影響範囲、脆弱なコードへの参照、リクエストパラメーターを含む:https://github.com/advisories/GHSA-rmmm-8g4j-3qgc(2026年11月7日確認)
  • CVE-2026-118066 レコード、2026年11月7日公開、CVSS v3.1 9.1、CWE-287:https://www.cve.org/CVERecord?id=CVE-2026-118066(2026年11月7日確認)
  • Wordfence の脅威インテリジェンスアドバイザリ、CVE-2026-118066、CNA レコード:https://www.wordfence.com/threat-intel/vulnerabilities/id/2f7f5dc8-8cf7-493d-b012-89ff36a4dc02(2026年11月7日確認)
  • LatePoint の変更履歴、5.7.7 は 2026年11月2日、5.7.8 は 2026年11月3日:https://wordpress.org/plugins/latepoint/#developers(2026年11月7日確認)
  • LatePoint lib/models/model.php 5.7.6、IS NOT NULL 分岐:https://plugins.trac.wordpress.org/browser/latepoint/tags/5.7.6/lib/models/model.php#L411(2026年11月7日確認)
  • LatePoint lib/helpers/customer_helper.php 5.7.6、get_by_account_nonse():https://plugins.trac.wordpress.org/browser/latepoint/tags/5.7.6/lib/helpers/customer_helper.php#L228(2026年11月7日確認)
  • LatePoint lib/controllers/customer_cabinet_controller.php 5.7.6、change_password():https://plugins.trac.wordpress.org/browser/latepoint/tags/5.7.6/lib/controllers/customer_cabinet_controller.php#L468(2026年11月7日確認)
  • LatePoint lib/controllers/steps_controller.php 5.7.6、start_from_order_intent():https://plugins.trac.wordpress.org/browser/latepoint/tags/5.7.6/lib/controllers/steps_controller.php#L187(2026年11月7日確認)
  • LatePoint チェンジセット 3779410、アドバイザリが参照するセキュリティ変更:https://plugins.trac.wordpress.org/changeset?reponame=&old=3779410%40latepoint&new=3779410%40latepoint(2026年11月7日確認)
  • LatePoint lib/models/model.php 5.7.8、マーカーオブジェクトの分岐:https://plugins.trac.wordpress.org/browser/latepoint/tags/5.7.8/lib/models/model.php#L455(2026年11月7日確認)
  • WordPress.org プラグイン API、latepoint、100,000 件の有効なインストールと 1,841,969 件のダウンロード:https://wordpress.org/plugins/latepoint/(2026年11月7日確認)
  • CISA の悪用済み脆弱性カタログ JSON フィード、2026年11月7日に CVE-2026-118066 を確認(未掲載):https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json(2026年11月7日確認)