The Exploit
An authenticated attacker with Subscriber-level access or higher can inject arbitrary SQL through the options[list_id] parameter in Icegram Express forms.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.local
Content-Type: application/x-www-form-urlencoded
Cookie: wordpress_logged_in=<subscriber_session>
action=ig_es_get_subscriber_lists&options[list_id]=1 UNION SELECT user_login,user_pass,user_email FROM wp_users--
The attacker receives a response that leaks the WordPress user table, including password hashes. Because the plugin fails to type-cast the list_id parameter before embedding it into a SQL WHERE clause, any SQL syntax in the parameter is executed as part of the same query. A UNION SELECT stacked query reveals sensitive data; a DELETE payload destroys records.
What the Patch Did
Before
if ( is_array( $list_ids ) ) {
$list_ids_str = implode( ',', $list_ids );
} else {
$list_ids_str = $list_ids;
}
$where = "id IN ({$list_ids_str})";
After
if ( is_array( $list_ids ) ) {
$list_ids = array_map( 'intval', $list_ids );
$list_ids_str = implode( ',', $list_ids );
} else {
$list_id = intval( $list_ids );
$list_ids_str = $list_id;
}
$where = "id IN ({$list_ids_str})";
The patch applies intval() to the $list_ids parameter before interpolation. intval() is PHP's integer-casting function; it parses the input as a decimal integer and strips all non-numeric characters. This ensures that only numeric values—not SQL syntax—reach the query string. The fix is applied conditionally: array elements are cast via array_map(), and scalar values are cast directly. No parameterized query ($wpdb->prepare()) is used, but explicit type conversion achieves the same defense by narrowing the input alphabet to only digits.
Root Cause
CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
The vulnerability originates in the options[list_id] request parameter, parsed by the AJAX handler ig_es_get_subscriber_lists. This value is passed to the ES_DB_Lists class method (implied by the file path) without sanitization. The dataflow is linear: request parameter → local variable $list_ids → string interpolation into $where → SQL IN clause. The trust boundary is crossed at the interpolation step: the plugin assumes $list_ids contains only numeric list IDs, but it does not validate or escape this assumption. An attacker with Subscriber access (the minimum role required to trigger AJAX endpoints in WordPress) can inject SQL keywords (UNION, SELECT, --) that alter the query structure.
Why It Works
The load-bearing line is intval( $list_ids ). Remove it, and the bug remains: an attacker can still pass 1 UNION SELECT ... as list_id, and the string will be embedded directly into the WHERE clause. intval() is the sole defense: it converts the string to an integer, truncating any appended SQL syntax. The other lines—the is_array() check and array_map()—are defense-in-depth: they handle the case where the caller provides an array of IDs (common in WordPress for bulk operations). If only a scalar were supported, a single intval() would suffice. The array_map() ensures that every element in an array is cast before the array is imploded, preventing an attacker from injecting SQL via any element of a multi-value parameter like options[list_id][0], options[list_id][1], etc.
Hardening Checklist
-
Use parameterized queries (
$wpdb->prepare()) for all user-controlled values in SQL. Example:$wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE ID IN (%d)", $id )for scalar values, or build the placeholder string dynamically for arrays. This is the WordPress-native, industry-standard defense and is simpler than manual type casting. -
Cast numeric IDs to
intval()orabsint()at the trust boundary (request handler) before passing to business logic. Do not assume the business layer will do it.absint()is preferred in WordPress when negative values are nonsensical. -
Audit all
implode()calls on user-controlled arrays. They are a common vector for this bug:implode()does not escape or type-cast its input. Search the codebase for patterns likeimplode( ',', $var )where$varoriginates from$_GET,$_POST, oroptions[]parameters. -
Use static analysis to flag unparameterized
$wpdbqueries. Tools like PHPStan with WordPress-specific rule sets will catch direct string interpolation into$wpdb->query(),$wpdb->get_results(), etc. Integrate into CI/CD. -
Require code review for any SQL construction, especially in database abstraction layers. The
ES_DB_Listsclass is a data layer; SQL construction at this level is high-risk because it affects many callers. Enforce a policy that all database code be reviewed by a second engineer before merge.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-4845