The Exploit
An unauthenticated attacker can perform SQL injection via the author__not_in parameter in any REST API endpoint that passes user input to WP_Query. The following curl request demonstrates the injection against the default /wp/v2/posts endpoint.
curl -X GET 'http://target.example.com/wp-json/wp/v2/posts?author__not_in[]=1+AND+(SELECT+5334+FROM+(SELECT(SLEEP(2)))gVUx)--+-'
If the target is vulnerable, the response will be delayed by approximately 2 seconds before returning a JSON payload of posts. A clean request without the injected delay will return with no measurable additional latency.
What the Patch Did
Before
if ( ! empty( $query_vars['author__not_in'] ) ) {
if ( is_array( $query_vars['author__not_in'] ) ) {
$query_vars['author__not_in'] = array_unique( array_map( 'absint', $query_vars['author__not_in'] ) );
sort( $query_vars['author__not_in'] );
}
$author__not_in = implode( ',', (array) $query_vars['author__not_in'] );
$where .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) ";
}
After
if ( ! empty( $query_vars['author__not_in'] ) ) {
$author__not_in_id_list = wp_parse_id_list( $query_vars['author__not_in'] );
if ( count( $author__not_in_id_list ) > 0 ) {
sort( $author__not_in_id_list );
$where .= sprintf(
" AND {$wpdb->posts}.post_author NOT IN (%s) ",
implode( ',', $author__not_in_id_list )
);
/** Update the query var for stable cache key generation in {@see self::generate_cache_key()}. */
$query_vars['author__not_in'] = $author__not_in_id_list;
}
The patch replaces the weak absint() sanitization (which only converts to an integer without validating the entire input) with wp_parse_id_list(), a WordPress API function that parses a list of IDs, removing any non-numeric values and returning only validated integers. This is an input validation control.
Root Cause
This vulnerability is a classic SQL Injection (CWE-89). The dataflow is: an attacker sends the author__not_in[] parameter in a REST API request to any endpoint that accepts WP_Query parameters (e.g., /wp/v2/posts). WordPress processes the parameter into $query_vars['author__not_in']. When the value is an array (as the [] suffix forces), the old code applies absint() to each element individually — but absint() simply returns the absolute integer value of whatever it receives, and for non-numeric strings it returns 0. Critically, the implosion into the SQL IN clause happens only if the value is detected as an array. However, the original code allowed the fallback to (array) $query_vars['author__not_in'] which, for a string input, wraps it into an array — meaning the absint() checks are entirely bypassed when author__not_in is passed as a scalar string, and even the array branch only converts values to integers without discarding non-numeric content that may contain SQL. The trust boundary is crossed at the point where user-supplied array elements are imploded directly into the SQL WHERE clause.
Why It Works
The single load-bearing line in the fix is the call to wp_parse_id_list($query_vars['author__not_in']). This function strips all non-numeric values from the input array entirely, not just converting them to integers. The old absint() pass would turn "1 AND (SELECT ...)" into 1, preserving the original array element count and position, but then imploding them together — and crucially, when a scalar string was passed rather than an array, the entire sanitization branch was skipped. The engineer added wp_parse_id_list() as a safer canonicalization function that discards non-integer entries, eliminating the possibility of SQL fragments surviving the sanitization. The other additions — explicit count() check and sprintf() usage — are defense-in-depth for consistent cache key generation and explicit SQL construction, but removing them would not make the bug exploitable again as long as wp_parse_id_list() remains.
Hardening Checklist
- Replace all manual
absint()loops over user-supplied arrays withwp_parse_id_list()from WordPress core, which discards non-integer entries entirely rather than converting them. - Never implode user-supplied array values directly into SQL
INclauses without first passing through a whiteslist validation function (likewp_parse_id_list()). - Use the WordPress REST API's built-in parameter validation (
sanitize_callbackandvalidate_callbackinregister_rest_route()) to reject unexpected data types at the endpoint registration level. - Treat all
WP_Queryparameter inputs passed through the REST API as untrusted and applywp_unslash()and type-specific sanitization before they reach query construction. - Add
wpdb::prepare()orsprintf()with%s/%dplaceholders for every value interpolated into a SQL string, even when the value is believed to be already sanitized.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-63030