The Exploit
An authenticated WordPress user with Editor role or above can inject arbitrary SQL by manipulating the search parameter in the Nelio AB Testing REST API endpoint.
POST /wp-json/nelio-ab-testing/v1/posts HTTP/1.1
Host: target.wordpress.local
Authorization: Bearer <EDITOR_TOKEN>
Content-Type: application/json
{
"search": "test' UNION SELECT user_login, user_pass FROM wp_users WHERE '1'='1"
}
The attacker receives a 200 response with database rows from wp_users leaked into the search result set. The injected UNION clause executes because the search parameter is passed into a LIKE clause without full SQL escaping — only wildcard escaping via $wpdb->esc_like(), which does not sanitize SQL metacharacters like single quotes.
What the Patch Did
Before
$term = $wpdb->esc_like( $term );
$term = ' \'%' . $term . '%\'';
$where .= ' AND ' . $wpdb->posts . '.post_title LIKE ' . $term;
After
$term = esc_sql( $wpdb->esc_like( $term ) );
$term = ' \'%' . $term . '%\'';
$where .= ' AND ' . $wpdb->posts . '.post_title LIKE ' . $term;
The patch wraps the already-escaped $term variable with esc_sql(), which applies full SQL escaping on top of the wildcard escaping. esc_sql() escapes single quotes, double quotes, and backslashes — the metacharacters that esc_like() alone does not defend against. Together, these two escaping functions create defence-in-depth: esc_like() neutralizes LIKE wildcards to prevent query logic bypass, while esc_sql() neutralizes SQL string delimiters to prevent injection into the string literal itself.
Root Cause
CWE-89: SQL Injection. The $term variable originates from the REST API request parameter search in the POST body. The parameter flows into line 458 at $wpdb->esc_like( $term ) without prior sanitization. Although esc_like() removes LIKE metacharacters (% and _), it does not escape SQL metacharacters. The value is then concatenated directly into the WHERE clause string on line 461 via $wpdb->posts . '.post_title LIKE ' . $term. The trust boundary—separating user input from SQL—is crossed without applying a SQL-specific escaping function. An authenticated attacker with Editor capability can therefore close the string literal with a single quote and append malicious SQL logic.
Why It Works
The load-bearing change is the addition of esc_sql() around the already-escaped $term. If you removed this wrapper, the bug remains fully exploitable: $wpdb->esc_like() alone escapes only wildcard characters, leaving SQL string delimiters unescaped. An attacker's quote character passes through untouched. The engineer added both functions not for redundancy but for orthogonal coverage: esc_like() defends the LIKE pattern syntax (wildcards must not be interpreted as regex), while esc_sql() defends the SQL string context (quotes and backslashes must not be interpreted as delimiters). Layering both ensures that a malicious search term cannot break out of either the LIKE clause logic or the string literal binding it.
Hardening Checklist
- Use
$wpdb->prepare()for all dynamic SQL. Instead of manual escaping and concatenation, pass parameterized queries:$wpdb->prepare( "AND post_title LIKE %s", '%' . $wpdb->esc_like( $term ) . '%' ). Prepared statements are the canonical defence against SQL injection in WordPress. - Audit all uses of
$wpdb->esc_like(). It escapes for LIKE syntax, not SQL syntax. Anyesc_like()call should be paired withesc_sql()or wrapped inprepare(), not used alone in string concatenation. - Run authenticated REST endpoint tests through a SQL injection fuzzer. Tools like SQLMap can be configured with WordPress session tokens to automatically detect injection in authenticated endpoints that maintainers often overlook.
- Apply
sanitize_text_field()to search inputs before business logic. While not a substitute for SQL escaping, it removes control characters and prevents some injection vectors from entering the codebase at all. - Use static analysis on all
$wpdbqueries. Linters like Psalm with WordPress rulesets flag concatenated queries and unescaped LIKE clauses; integrate them into CI/CD pipelines.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-25378