我们的 AI 引擎 Neutron 在加州大学伯克利分校的 CyberGym 基准测试中取得了 97.1% 的成绩。 了解更多

深度解析

深度解析

LatePoint 预订插件账户接管(CVE-2026-118066)

LatePoint 中的未认证 SQL 注入让攻击者可以接管客户账户。5.7.7 已修复,5.7.8 完成加固。

TL;DR

  • 未认证攻击者可以把字符串 IS NOT NULL 作为密码重置令牌发送,从而重置 LatePoint 客户账户的密码。Wordfence 报告了该漏洞,CVE 记录致谢 Jakub Herman,并于 2026 年 11 月 7 日发布。
  • LatePoint 5.7.6 及更早版本受影响。该插件有 100,000 个活跃安装。漏洞位于把哨兵值转换为 SQL 的查询构建器中,可通过两个公开 AJAX 路由访问。
  • CVE 记录将其评为 CVSS v3.1 9.1 严重。未报告在野利用,截至 2026 年 11 月 7 日该 CVE 不在 CISA KEV 目录中。
  • 请更新到 5.7.8。如果无法更新,请封锁文中记录的两个请求模式,并审计客户密码重置;参见“如何修复?”。

我受影响吗?

只有同时满足以下所有条件时,您才会受影响:

  • 站点运行 LatePoint 插件 5.7.6 或更早版本。
  • 插件处于启用状态,因此其公开 AJAX 路由会响应请求。
  • 至少有一条 account_nonse 非空的客户记录。LatePoint 会为客户创建该值,因此只有一个预订的站点也满足条件。
  • 攻击者能够获知客户电子邮件地址。公告中的攻击链从订单意图中读取一个地址;公开的预订确认或员工目录也可能提供地址。
  • 对于关联 WordPress 账户的影响,wp_users_as_customers 选项已启用。

易受攻击的代码位于插件的核心客户类和查询类中。不需要任何可选模块。这两个路由注册给未登录访客,因此攻击者不需要站点账户。

分支 受影响版本 修复版本
LatePoint 5.7.6 及更早版本 5.7.7(2026 年 11 月 2 日),随后 5.7.8(2026 年 11 月 3 日)完成模型修复

CVE 记录将 5.7.6 及更早版本列为受影响。5.7.7 封锁了两个已记录的请求路径。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 应用防火墙规则可以减少暴露,但无法修复代码。需要封锁的两个请求模式是:

  • 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 的规则会漏掉其他形式。只有当真实令牌包含空格时,该规则才会封锁合法重置,而插件的十六进制令牌从不包含空格。

供应商未列出任何变通方法。限制可访问客户中心的账户没有帮助,因为这些路由按设计就是公开的。

根本原因是什么?

当 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 非空的客户都会匹配,set_limit(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 版本在另一个查询中加入了 is_scalar() 检查,即 OsStepsHelper::set_customer_object() 中的客户 uuid。该检查拒绝嵌套数组,但接受标量字符串 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 发送 POST 请求,参数为 params[password_reset_token]=IS NOT NULL 和新密码。端点通过哨兵查找客户,并返回第一个匹配行,公告将其描述为 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 客户账户,并读取或修改该客户的预订、订单和个人数据。被覆盖的账户是哨兵查询返回的第一行,公告将其描述为 ID 最小的客户。

影响取决于以下条件:

  • 站点运行 LatePoint 5.7.6 或更早版本,且插件已启用。
  • 攻击者可以访问公开 AJAX 路由。
  • 客户记录的 account_nonse 非空。
  • 攻击者可以获知客户电子邮件地址并为其请求重置。
  • 密码更改请求到达时,客户的重置时间戳在 15 分钟窗口内。
  • 对于关联的 WordPress 账户,wp_users_as_customers 已启用。

CVE 记录将机密性和完整性评为高,可用性评为无。攻击者可以读取客户数据并更改密码,但该漏洞不执行代码,也不改变安全范围。插件仅将 WordPress 账户关联到客户安全角色,因此关联账户是订阅者或客户,而不是管理员,除非站点更改了该配置。

公告没有展示通往 WordPress 管理员账户的路径。它描述的是客户账户接管,在启用该选项时还包括关联的 WordPress 订阅者或客户账户。

为什么修复有效?

修复阻止请求字符串到达 SQL 关键字分支。LatePoint 5.7.7 在两个已记录入口加入严格的字符检查。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 发布,包含两个入口防护和标记辅助函数。
  • 2026 年 11 月 3 日:LatePoint 5.7.8 发布,从查询构建器中移除字面量 IS NOT NULL 分支。
  • 2026 年 11 月 7 日:CVE 记录于 11:26 UTC 发布,公告 GHSA-rmmm-8g4j-3qgc 于 12:31 UTC 发布。

两个修复版本在两天内发布,CVE 记录在 5.7.8 之后四天发布。
图 1:从 5.7.7 修复到 5.7.8 和 CVE 记录。来源:LatePoint 更新日志和 CVE 记录,2026 年 11 月 7 日检查

常见问题

攻击者需要站点账户吗?

不需要。这两个路由注册给未登录访客,密码更改端点在收到重置令牌时跳过 nonce 检查。攻击者只需要对站点的网络访问。

5.7.7 足够吗?

它封锁了两个已记录的请求路径。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 应用防火墙封锁两个请求模式,并每天审计重置时间戳。防火墙规则减少暴露,但不修复查询构建器。

请求失败是否证明站点安全?

不能。哨兵返回第一个匹配的客户,因此即使攻击者为重置计时器猜错了电子邮件,请求也可能成功。失败的密码更改仍可能匹配了某个客户,只在新鲜度检查时失败。请同时检查日志和重置行。

参考文献

  • 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 日检查)