WooCommerce 加购 URL 的 nonce 校验为何不生效,如何防范机器人请求?
结论
可以用 nonce 限制缺少有效令牌的加购 URL,但它只能作为补充措施,不能单独防住 botnet。现有代码在条件判断和请求覆盖范围上都有问题;仅凭这些代码,无法确定本地测试未拦截的唯一原因。
修改校验逻辑前,先确认实际请求是否包含 add-to-cart 查询参数、是否经过 woocommerce_add_to_cart_validation,以及购物车是否真的发生变化。
原代码为什么可能没有拦截
AJAX 判断会跳过 nonce 校验
原条件是:
! defined( 'DOING_AJAX' ) && ! wp_verify_nonce( ... )
defined() 只判断常量是否存在。如果 DOING_AJAX 已被定义,即使值为 false,整个条件也不会成立,wp_verify_nonce() 甚至不会执行。
判断 AJAX 状态通常应使用 wp_doing_ajax(),它默认同时检查常量是否定义及其值。但 AJAX 状态不能证明请求来自真人,不适合作为免校验的依据。WordPress 的 wp_doing_ajax() 文档
按钮的 URL 不一定是实际请求地址
代码最外层只检查:
isset( $_GET['add-to-cart'] )
如果点击按钮后实际发送的是 ?wc-ajax=add_to_cart,并通过 POST 提交 product_id,请求就可能没有 add-to-cart 查询参数,整个校验分支会被跳过。WooCommerce 的传统 AJAX 加购处理会从 POST 读取商品信息,并调用验证过滤器。WooCommerce AJAX 加购源码
链接里有 _wpnonce,也不代表 AJAX 请求提交了它。首页商品区块、标签页和属性页分别使用哪条请求路径,需要在浏览器的 Network 面板中确认。
返回 false 不代表 HTTP 请求被拒绝
woocommerce_add_to_cart_validation 返回 false 会阻止相应处理流程继续加购,但不保证返回 HTTP 403。
测试时应检查购物车数量是否增加,以及错误提示或响应内容是否变化。仅看到 HTTP 200,不能认定校验失效。
原代码还直接读取 $_REQUEST['_wpnonce'],没有处理参数缺失或数组输入,也需要修正。
如何修正直接加购 URL 的校验
如果当前目标是限制带有 ?add-to-cart=... 的传统加购请求,可以保留第一个生成 nonce 的函数,将第二个校验函数及其 add_filter() 替换为:
add_filter(
'woocommerce_add_to_cart_validation',
'site_validate_add_to_cart_url_nonce',
20,
3
);
function site_validate_add_to_cart_url_nonce( $passed, $product_id, $quantity ) {
if ( ! $passed ) {
return false;
}
// This policy applies only to requests with add-to-cart in the query string.
if ( ! isset( $_GET['add-to-cart'] ) ) {
return $passed;
}
$nonce = '';
if ( isset( $_GET['_wpnonce'] ) && is_string( $_GET['_wpnonce'] ) ) {
$nonce = sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) );
}
if ( ! wp_verify_nonce( $nonce, 'woocommerce-add-to-cart' ) ) {
wc_add_notice(
'安全校验失败,请刷新商品页面后重新加入购物车。',
'error'
);
return false;
}
return $passed;
}
这段代码从 URL 中读取 nonce,不再因为 AJAX 状态而放行带有 add-to-cart 查询参数的请求。
woocommerce-add-to-cart 是这里创建与验证双方约定的 action 字符串。使用这个名称,并不意味着所有 WooCommerce 加购入口都会自动生成或验证它。
WooCommerce 的传统简单商品加购处理会调用该过滤器,并在验证通过后添加商品。自定义处理器是否遵循相同流程,需要结合实际插件和版本确认。WooCommerce 表单处理源码
这段修正的适用范围
这段代码只覆盖带有 add-to-cart 查询参数且经过该过滤器的请求,不能覆盖所有加购入口。
如果还要校验 AJAX 或 Store API,必须为对应入口设计令牌传递和服务端验证。不能直接删除上述范围判断,要求所有请求携带 _wpnonce,否则尚未提交该参数的正常加购流程也会失败。
如何确认校验确实执行
在已关闭缓存的本地环境中,选一个可正常购买的简单商品,分别验证:
- 直接访问不带
_wpnonce的?add-to-cart=商品ID,确认购物车数量没有增加。 - 添加错误的
_wpnonce,确认仍无法加购。 - 使用当前页面生成、属于当前用户会话的有效 nonce,确认能够正常加购。
- 点击首页、标签页和属性页的加购按钮,在 Network 面板检查请求地址、方法及参数。
如果直接访问无 nonce 的 URL 仍然增加商品,可以临时在回调入口添加一条不含令牌或 Cookie 的日志:
error_log( 'site_validate_add_to_cart_url_nonce reached' );
如果没有日志,先检查插件是否加载、过滤器是否在请求处理前注册,以及实际加购入口是否调用了该过滤器。如果有日志但仍然加购,再检查是否存在其他处理器或后续过滤器改变验证结果。排查完成后移除日志。
测试前记录购物车原有数量,避免把已经存在的商品误认为本次请求添加成功。
nonce 能防住多少机器人请求
WordPress nonce 主要用于缓解 CSRF,在有效期内可以重复使用。默认情况下,未登录访客使用相同的用户 ID 0;在相同 action 和时间窗口下,生成的 nonce 也相同。WordPress nonce 文档
机器人一旦获取公开页面上的有效 nonce,就可能继续请求带令牌的 URL。它目前似乎没有抓取页面,但不能保证以后不会调整行为。
针对大量直接加购请求,可以结合访问日志,在 CDN、WAF 或 Web 服务器层针对实际入口设置限速、挑战或拦截规则,尽量减少进入 PHP 的恶意流量。分布式请求需要综合请求行为和会话等信号,单靠每个 IP 的固定阈值可能不够。
上线前还应确认两点:强制 nonce 会使不带令牌的历史直达加购链接失效;如果以后启用页面缓存,需要避免缓存中的过期 nonce 导致正常用户加购失败。原有 .htaccess 规则为什么漏掉约 10% 的请求,需要查看规则和漏网请求样本才能判断。
备注:内容仅供参考。
