The Exploit
An unauthenticated attacker can extract arbitrary data from the WordPress database by injecting SQL time-delay payloads into the active_status parameter of any Events Manager query endpoint.
GET /wp-admin/admin-ajax.php?action=em_events_load&active_status=1 AND SLEEP(5)-- HTTP/1.1
Host: target.example.com
User-Agent: Mozilla/5.0
Accept: */*
Connection: close
The response hangs for approximately 5 seconds before returning, confirming SQL injection. An attacker observes the artificial delay in response time — a side effect of the SLEEP() function executing within the database query. By measuring response times across multiple payloads, an attacker can exfiltrate sensitive data (user credentials, email addresses, event booking details) one bit at a time using Boolean-based or time-based blind SQL injection techniques, requiring no authentication whatsoever.
What the Patch Did
Before
if ( $args[$status_opt] == 1 ) {
$active_statuses['include'][] = $status;
} elseif( $args[$status_opt] !== null ) {
$active_statuses['exclude'][] = $status;
}
// ...
foreach ( $active_status_array as $status ){
if ( $status > 0 ) {
$active_statuses['include'][] = $status;
} else {
$active_statuses['exclude'][] = absint($status);
}
}
After
if ( $args[$status_opt] == 1 ) {
$active_statuses['include'][] = absint($status);
} elseif( $args[$status_opt] !== null ) {
$active_statuses['exclude'][] = absint($status);
}
// ...
foreach ( $active_status_array as $status ){
if ( $status > 0 ) {
$active_statuses['include'][] = absint($status);
} else {
$active_statuses['exclude'][] = absint($status);
}
}
The patch applies WordPress's absint() sanitization function — which casts a value to an absolute integer — to all code paths that process status values. Previously, the absint() call appeared only in the else branch (line 624 of the original), leaving two conditional branches unprotected. The fix ensures that every status value entering the database query is coerced to an integer type before concatenation, eliminating the ability to inject SQL syntax via string manipulation.
Root Cause
This is CWE-20 (Improper Input Validation) degrading into CWE-89 (SQL Injection). The active_status parameter flows from user input (AJAX request) into the $args array without type validation. The code branches on conditional logic—checking whether $args[$status_opt] equals 1 or is non-null—but fails to sanitize the actual $status value in two branches before it reaches the SQL query builder. The unprotected $status variable is concatenated into a WHERE clause without prepared statements or consistent escaping. An attacker can supply 1 AND SLEEP(5)-- as the parameter value, and because the conditional checks only the presence or equality of the parameter (not its type), the unsanitized string is passed directly to the query construction logic.
Why It Works
The load-bearing line is absint($status) itself. If you removed all four absint() calls and restored the original code, the bug would remain fully exploitable—the attacker's SQL syntax would still execute. The security fix works because absint() is a WordPress core function that performs both type coercion and bounds checking: it converts the input to an integer (stripping all SQL metacharacters), and if the input is not a valid numeric string, it returns 0. This means 1 AND SLEEP(5)-- becomes 1 (the string is parsed as the integer 1 and the rest discarded), and the injected SQL is neutralized before it ever reaches the database. The engineer added absint() to all four branches because SQL injection is a path-based vulnerability—an attacker only needs one unprotected code path to succeed. By ensuring every conditional branch sanitizes the value consistently, the patch eliminates the inconsistency that made the vulnerability exploitable in the first place.
Hardening Checklist
- Use prepared statements with placeholders (
$wpdb->prepare()) instead of string concatenation for all user-supplied values in SQL queries. Even ifabsint()is applied, parameterized queries provide defence-in-depth against type confusion and future logic errors. - Apply
absint(),intval(), orsanitize_text_field()at the point of input receipt, not just before database use. Store the sanitized value immediately in$_POST,$_GET, or a validated request object to prevent accidental reuse of the raw input later. - Add a linting rule or PHPCS sniff to detect string concatenation in SQL queries (
$wpdb->query(),$wpdb->get_results()) and flag unparameterized user variables. Tools like WordPress Coding Standards can catch this pattern at commit time. - Write integration tests that pass SQL injection payloads (e.g.,
1 OR 1=1,1; DROP TABLE,SLEEP(5)) to every AJAX action and ensure they return expected results without altering the database or delaying response. This catches logic errors in sanitization. - Audit all
$wpdb->prepare()calls to ensure placeholders are used for dynamic values, not formatted strings like"WHERE status = %d"with variables already cast. The placeholder syntax ensures the value is escaped according to its declared type.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-11260