Bug Report: botsany Anti-Bot Engine Blocks All Anonymous wp_ajax_nopriv_* Requests
WP Cerber Version: 9.7
File: cerber-load.php
Severity: Critical - causes mass false positives AND repeated “Error establishing a database connection” on any site using standard WordPress AJAX
Affected setting: Anti-Spam → “Protect all forms on the website with bot detection engine” (botsany)
Summary
When botsany is enabled and botssafe is disabled (both defaults), WP Cerber blocks all anonymous AJAX POST requests (wp_ajax_nopriv_*) with HTTP 403 and logs them as CRB_EV_SFD (“Spam form submission denied”). This affects any theme or plugin that uses WordPress’s standard public AJAX API.
The root cause is that cerber_is_bot() and cerber_is_antibot_exception() only exempt logged-in users and the heartbeat action from anti-bot token verification. All other anonymous AJAX calls are required to carry hidden form fields that are structurally impossible to include in programmatic JavaScript AJAX requests.
Reproduction
- Install any theme or plugin that uses
wp_ajax_nopriv_*hooks (e.g., themes with AJAX search, AJAX pagination, AJAX view counters, or any WooCommerce AJAX add-to-cart on product listing pages). - Enable Anti-Spam → Protect other forms (
botsany = 1). - Leave Safe AJAX mode disabled (
botssafe = 0, which is the default). - As a non-logged-in visitor, trigger any frontend AJAX action.
- Observe: the request returns HTTP 403, and
cerber_logrecords activity 17 (CRB_EV_SFD).
Scale of impact: On a high-traffic site, this generates thousands of false-positive rows per hour in both cerber_log and cerber_traffic tables. The write pressure accumulates fast enough to exhaust the database connection pool, causing repeated temporary “Error establishing a database connection” outages visible to real visitors - a direct site availability problem caused entirely by false positives from this bug.
Root Cause Analysis
Three functions in cerber-load.php contain the same structural flaw: they only exempt logged-in users and heartbeat from anti-bot checks on AJAX requests, leaving all wp_ajax_nopriv_* actions unprotected.
1. cerber_is_antibot_exception() - line 2891
// cerber-load.php, line 2891
function cerber_is_antibot_exception(){
// ...
if ( is_admin() ) { // TRUE for admin-ajax.php
if ( cerber_is_wp_ajax() ) { // TRUE (DOING_AJAX is defined)
if ( is_user_logged_in() ) { // FALSE for anonymous visitors
return true; // [*] only logged-in users exempted
}
if ( class_exists( 'WooCommerce' ) ) {
// [*] only a specific WooCommerce background process exempted
}
// [*] anonymous wp_ajax_nopriv_* falls through here -- NOT exempted
}
else {
return true;
}
}
// ... returns false -> not exempt
}
Problem: Anonymous AJAX requests reach the end of the is_admin() block without hitting any return true. The function proceeds to check comments, XML-RPC, REST API, etc. - none of which match an AJAX request. The function returns false.
2. cerber_is_bot() - line 3459
// cerber-load.php, line 3479
if ( is_admin() ) {
if ( cerber_is_wp_ajax() ) {
if ( is_user_logged_in() ) {
$ret = false; // [*] logged-in: not a bot
}
elseif ( ! empty( $_POST['action'] ) ) {
if ( $_POST['action'] == 'heartbeat' ) {
$ret = false; // [*] heartbeat: not a bot
}
// [*] ALL other nopriv actions: $ret stays null -> continues to token check
}
}
else {
$ret = false;
}
}
Problem: For any anonymous AJAX action other than heartbeat, $ret remains null at line 3497, and execution continues to the anti-bot token verification at line 3553.
3. cerber_antibot_mode() - line 2985
// cerber-load.php, line 2985
function cerber_antibot_mode() {
if ( current_user_can( 'manage_options' ) ) {
return 2; // Admins: cookies only
}
if ( cerber_is_wp_ajax() ) {
if ( crb_get_settings( 'botssafe' ) ) {
return 2; // Safe mode ON: cookies only
}
if ( ! empty( $_POST['action'] ) ) {
if ( $_POST['action'] == 'heartbeat' ) {
return 2; // Heartbeat: cookies only
}
}
}
// ...
return 1; // [*] Default: Cookies + Hidden Fields
}
Problem: When botssafe = 0 (default), anonymous AJAX returns mode 1. Mode 1 requires hidden form fields in $_POST:
// cerber-load.php, line 3561
if ( $mode == 1 ) {
foreach ( $antibot[0] as $fields ) {
if ( empty( $_POST[ $fields[0] ] ) || $_POST[ $fields[0] ] != $fields[1] ) {
$ret = true; // [*] FLAGGED AS BOT
break;
}
}
}
AJAX requests are programmatic XMLHttpRequest/fetch POST calls made by JavaScript. They send structured data payloads (action=xxx¶m=yyy), not HTML form submissions. They never contain Cerber’s dynamically-generated hidden form fields because:
- Cerber injects hidden fields into rendered HTML
<form>elements viacerber_antibot_code() - AJAX calls construct their POST body in JavaScript, independent of any HTML form
- There is no mechanism for third-party JavaScript to discover and include Cerber’s dynamically-named fields
This means mode 1 will always fail for any legitimate AJAX request, regardless of the plugin or theme making the call.
Evidence from Production
Database export from a live site showing the impact:
| Table | Total rows | Rows caused by this bug | Percentage |
|---|---|---|---|
cerber_log |
~51,479 | ~49,763 (activity 17, botsany) |
96.7% |
cerber_traffic |
~21,998 | ~21,998 (HTTP 403 on AJAX POST) | 100% |
Why tables fill so fast
Each page visit by an anonymous user triggers multiple parallel AJAX POST requests - not one. A typical page with live search, a view counter, lazy-loaded content, and a rating widget can generate 4-8 POST requests per page load. Every single one of those requests:
- Is intercepted by
cerber_post_control(). - Fails the
cerber_is_bot()token check (mode 1, missing hidden fields). - Writes one row to
cerber_log(activity 17) and one row tocerber_traffic(HTTP 403).
At modest traffic of 200 anonymous visitors per minute, this produces:
200 visitors/min x 6 AJAX calls/visitor x 2 rows/call = 2,400 INSERT operations per minute
= 144,000 rows per hour
= 3,456,000 rows per day
MySQL’s InnoDB engine serialises writes to these tables. At this INSERT rate, the database connection pool saturates and WordPress starts returning “Error establishing a database connection” to real visitors - not because the server is down, but because legitimate page loads cannot acquire a connection while Cerber’s false-positive writes are monopolising them.
This bug turns WP Cerber itself into a denial-of-service vector against the site it is supposed to protect.
All blocked requests came from legitimate visitors with real browsers (Chrome, Safari, Samsung Browser, iOS Safari) performing standard frontend AJAX operations. User agents and referer headers confirm these are real users browsing - not bots.
The pattern is not specific to any single theme or plugin. The following examples show common, legitimate wp_ajax_nopriv_* actions registered by widely-used WordPress components - all of which would be blocked by this bug:
| Plugin / Component | AJAX action | Purpose |
|---|---|---|
| Any custom theme | theme_search, load_more_posts |
Live search, infinite scroll |
| WooCommerce | woocommerce_get_refreshed_fragments |
Mini-cart update |
| Contact Form 7 | wpcf7_submit |
Form submission |
| Elementor | elementor_ajax |
Widget interactions |
| BuddyPress | bp_ajax_query_string |
Activity feed loading |
| The Events Calendar | tribe_calendar_json_grid |
Event grid loading |
| bbPress | bp-logout |
Session handling |
| Any membership plugin | check_membership, verify_access |
Content gating |
Any plugin or theme that registers add_action( 'wp_ajax_nopriv_my_action', ... ) - which is the officially documented WordPress pattern for anonymous AJAX - will produce false positives when botsany is active and botssafe is disabled.
Suggested Fix
Option A: Automatically use mode 2 (cookies only) for all AJAX requests
In cerber_antibot_mode(), AJAX requests should always use mode 2 regardless of botssafe. The cookie check alone is sufficient proof that JavaScript executed on the page (bots that don’t execute JS won’t have the cookies):
// cerber-load.php, cerber_antibot_mode()
function cerber_antibot_mode() {
if ( current_user_can( 'manage_options' ) ) {
return 2;
}
if ( cerber_is_wp_ajax() ) {
return 2; // AJAX always uses cookies-only mode
}
// ... rest of function unchanged
}
Rationale: If DOING_AJAX is true, the request came through admin-ajax.php, which means WordPress has already validated the request path. Requiring hidden form fields for AJAX is architecturally unsound because AJAX payloads are constructed in JavaScript, not submitted from HTML forms. The cookie check (mode 2) is the correct anti-bot mechanism for AJAX: it verifies that JavaScript executed in the visitor’s browser and set the anti-bot cookies, which is exactly what distinguishes real browsers from simple HTTP bots.
Option B: Exempt wp_ajax_nopriv_* actions that have registered handlers
In cerber_is_bot(), after the heartbeat check, verify if the action has a registered wp_ajax_nopriv_* handler. If it does, it’s a legitimate WordPress AJAX action - not a spam form submission:
// cerber-load.php, cerber_is_bot(), inside the is_admin() + cerber_is_wp_ajax() block
elseif ( ! empty( $_POST['action'] ) ) {
if ( $_POST['action'] == 'heartbeat' ) {
$ret = false;
}
// Check if a nopriv handler is registered for this action
elseif ( has_action( 'wp_ajax_nopriv_' . $_POST['action'] ) ) {
$ret = false;
}
}
Rationale: If a wp_ajax_nopriv_* handler is registered, the action was intentionally created by a plugin or theme for anonymous use. Blocking it as spam contradicts the site administrator’s intent.
Recommended approach
Option A is the safest and most correct fix. It addresses the root cause: hidden form fields are the wrong verification mechanism for AJAX requests. Cookie verification is both sufficient and appropriate - if a visitor’s browser executed Cerber’s JavaScript and set the anti-bot cookies, that is definitive proof the visitor is not a simple HTTP bot. The hidden-fields check adds no security value for AJAX and causes 100% false positives.
Workaround (current)
Users can mitigate this today by enabling:
Anti-Spam → Adjust anti-spam engine → Enable safe AJAX mode (botssafe = 1)
This switches AJAX to mode 2 (cookies only), which resolves the false positives. However, this setting is disabled by default and is described as “may increase spam risk slightly,” which discourages users from enabling it. In practice, mode 2 for AJAX does not increase spam risk because the cookie check already validates JavaScript execution.
Files and Lines Reference
| Location | Description |
|---|---|
cerber-load.php:2839-2884 |
cerber_post_control() - calls cerber_is_bot('botsany') and denies with 403 |
cerber-load.php:2864-2867 |
The specific branch that flags the request and logs CRB_EV_SFD |
cerber-load.php:2891-2977 |
cerber_is_antibot_exception() - missing exemption for anonymous AJAX |
cerber-load.php:2898-2912 |
The is_admin() + cerber_is_wp_ajax() block that only exempts logged-in users |
cerber-load.php:2985-3014 |
cerber_antibot_mode() - returns mode 1 for anonymous AJAX when botssafe=0 |
cerber-load.php:3459-3586 |
cerber_is_bot() - main bot detection function |
cerber-load.php:3479-3495 |
The AJAX exemption block that only covers logged-in and heartbeat |
cerber-load.php:3561-3567 |
Mode 1 hidden-field check that always fails for AJAX requests |
cerber-common.php:97 |
const CRB_EV_SFD = 17 - the false-positive activity code |
cerber-common.php:1566-1577 |
cerber_is_wp_ajax() - checks DOING_AJAX constant |
cerber-settings.php:389-390 |
Default values: botsany = 0, botssafe = 0 |