The Exploit
An authenticated subscriber-level user can inject arbitrary SQL through the b2sSortPostType parameter to extract sensitive database records.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.wordpress.local
Content-Type: application/x-www-form-urlencoded
Cookie: wordpress_logged_in=<subscriber_session>
action=b2s_get_posts&b2sSortPostType=post' UNION SELECT user_login,user_pass,user_email FROM wp_users WHERE '1'='1
When submitted by a subscriber account, the AJAX handler processes the b2sSortPostType parameter directly into a SQL LIKE clause without escaping. The attacker observes a response containing rows from wp_users — WordPress usernames, password hashes, and email addresses leak into the JSON response. No admin privileges required; the vulnerable endpoint accepts requests from any logged-in user.
What the Patch Did
Before
$postTypes .= " posts.`post_type` LIKE '%" . $this->searchPostType . "%' ";
After
$postTypes .= $wpdb->prepare(' AND posts.`post_type` LIKE %s', '%' . esc_sql($wpdb->esc_like($this->searchPostType)) . '%');
The patch introduced three security controls working in tandem. First, $wpdb->prepare() replaces unsafe string concatenation with parameterized query syntax, segregating SQL code from data. Second, $wpdb->esc_like() escapes the % and _ wildcard metacharacters that LIKE operators interpret, preventing an attacker from using them as query delimiters. Third, esc_sql() applies an additional HTML entity and quote-escaping layer as defense-in-depth. Together they form a three-layer input validation boundary: parameterization (structural), metaescaping (contextual), and encoding (deep). The patch also corrected the missing AND operator and restructured the conditional logic.
Root Cause
CWE-89: SQL Injection. The searchPostType parameter arrives in the AJAX request body, flows directly into the $this->searchPostType instance variable set in the class constructor, and then concatenates unsanitized into a SQL LIKE expression at line 157 of includes/B2S/Post/Item.php. The attacker-controlled value crosses the trust boundary between user input and SQL code without any escaping or parameterization, allowing injection of arbitrary SQL operators, keywords, and UNION clauses. Because the endpoint requires only subscriber-level authentication, any valid user account—including those created by third-party account registration plugins—can trigger the vulnerability.
Why It Works
The load-bearing line is $wpdb->prepare(). Without it, all three escaping functions become cosmetic: an attacker can close the quoted string with ', inject SQL, and comment out the remainder with --. Even if esc_like() were applied to a concatenated string—the pre-patch pattern—it would arrive at the database inside a vulnerable query template. $wpdb->prepare() enforces structural separation by moving user data into a bound parameter slot (%s) that the database driver cannot interpret as SQL syntax; the database engine treats everything after the %s token as a literal string value, not code. The esc_like() call is still necessary because LIKE operators have their own metasyntax (% and _ for wildcards), and prepare() does not escape those. esc_sql() is redundant given prepare() + esc_like(), but adds insurance against edge cases in database driver implementations or future code refactoring. The engineer layered all three to guarantee that a future developer removing one or two would still see protection from the strongest control.
Hardening Checklist
- Adopt
$wpdb->prepare()with placeholders (%s,%d,%i) for all user-supplied values in SQL queries; never concatenate or interpolate directly. - Call
$wpdb->esc_like()beforeprepare()on any parameter destined for a LIKE clause, even after parameterization, to escape%and_wildcards. - Audit authentication checks at the AJAX handler entry point: add
current_user_can()gates to reject subscribers from sensitive queries (e.g.,current_user_can('edit_others_posts')for post type enumeration). - Use static analysis tools such as PHPCS with WordPress security rulesets (e.g.,
WordPress.DB.PreparedSQLin WordPress Coding Standards) in CI/CD to flag unparameterized SQL patterns before merge. - Require code review sign-off for any AJAX handler that processes user input; parameterized queries are not a coding convention but a security mandate.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-3549