The Exploit
Unauthenticated attacker, no authentication required.
GET /wp-admin/admin-ajax.php?action=um_member_directory_search&search=test'+AND+SLEEP(5)-- HTTP/1.1
Host: target.wordpress.local
User-Agent: Mozilla/5.0
The server delays response by 5 seconds. A time-based blind SQL injection: each SLEEP() call in the injected SQL executes, leaking database schema and credential rows one bit at a time through response latency.
What the Patch Did
Before
$regexp_map = array(
'/select(.*?)from/im',
'/select(.*?)sleep/im',
'/select(.*?)database/im',
'/select(.*?)where/im',
'/update(.*?)set/im',
After
$regexp_map = array(
'/select(.*?)from/im',
'/select(.*?)sleep/im',
"/sleep\(\d+\)/im", // avoid any sleep injections
'/select(.*?)database/im',
'/select(.*?)where/im',
'/update(.*?)set/im',
The patch adds a new regex rule /sleep\(\d+\)/im to the input validation filter. This rule matches sleep() function calls anywhere in the search parameter — not just when preceded by select. The plugin's existing blacklist caught select.*sleep patterns but failed to block bare sleep() invocations, leaving the injection vector open. The new pattern closes that gap by rejecting the function call syntax directly.
Root Cause
CWE-20: Improper Input Validation; CWE-89: SQL Injection.
The search parameter in the um_member_directory_search AJAX action flows into the member directory query builder without prepared statements. The plugin attempts blacklist-based mitigation using regex patterns on line 1712 of class-member-directory.php, but the pattern /select(.*?)sleep/im only blocks the sequence "SELECT ... SLEEP" — it does not block a standalone SLEEP() call. An attacker supplies test' AND SLEEP(5)-- as the search value, which bypasses the regex (no select keyword in this substring) and reaches the underlying SQL query. The parameter is concatenated into the query string directly, breaking the SQL grammar and injecting a time-based delay function that executes in the database context.
Why It Works
The single load-bearing line is /sleep\(\d+\)/im, which detects the function call syntax itself. Without it, an attacker bypasses the old pattern by avoiding the word select and invoking sleep() directly in a WHERE clause. The other regex rules (/select.*from/im, /update.*set/im) remain necessary to catch DDL and DML payloads, but they are insufficient on their own because they require specific SQL keywords. The engineer added the sleep() pattern specifically for time-based blind injection, which is the most practical attack against this sink — response latency requires no data exfiltration and works through any network conditions. The pattern \(\d+\) (parenthesis around digits) matches the function-call syntax, not the word itself, which prevents false positives on comments containing the word "sleep".
Hardening Checklist
-
Use prepared statements everywhere: Replace all direct string concatenation in SQL queries with
$wpdb->prepare()on every variable placeholder. Blacklist filters are defence-in-depth only, never the primary control. -
Implement strict input whitelisting, not blacklisting: Define what characters are allowed in the
searchparameter (e.g.,[a-zA-Z0-9_\- ]) and reject anything outside that set usingpreg_match()before any processing. -
Add automated SAST scanning in CI/CD: Deploy tools like
phpcswith theWordPress-Securityruleset to flag direct SQL concatenation and missing$wpdb->prepare()calls before code reaches production. -
Validate regex patterns against known bypasses: For any input filter based on regex, maintain a test suite of common SQL injection patterns (time-based, union-based, boolean-based) and verify rejection. The bug here stemmed from untested assumptions about attack patterns.
-
Log and monitor SQL syntax errors: Enable
define('WP_DEBUG_LOG', true)in production and alert on repeated SQL syntax errors, which often indicate injection attempts hitting the blacklist.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-1702
- Ultimate Member Changelog: https://github.com/ultimatemember/ultimatemember (affected versions ≤ 2.10.0)