CVE-2026-2232 Product Table and List Builder for WooCommerce Lite Vulnerable To Unauthenticated Time-Based SQL Injection via ‘search’ Parameter

Overview
Published: 2026-02-19 CVE-ID: CVE-2026-2232 CVSS: 7.5 High Affected Plugin: Product Table and List Builder for WooCommerce Lite Affected Versions: <= 4.6.2 Vulnerability Type: Unauthenticate Time-Based SQL Injection CWE: CWE-89 Improper Neutralization of Special Elements used in an SQL Command
Description
The Product Table and List Builder for WooCommerce Lite plugin for WordPress is vulnerable to time-based SQL Injection via the ‘search’ parameter in all versions up to, and including, 4.6.2 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for unauthenticated attackers to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.
Patch And Commit Analysis

Based on Application’s changelog, I will compare between version 4.6.3 (Secure Version) and 4.6.2 (Vulnerable Version).

Technical Deep Dive: The “Sink” and Logical Failure
The core of this vulnerability lies in the wcpt_search__query function within search.php. While the developer attempted to use $wpdb->esc_like() and esc_sql(), the fundamental mistake was how the final SQL string was assembled.
Broken Query Concatenation
The plugin does not pass a complete, sanitized query. Instead, it passes a “fragmented” query string (e.g., … AND post_title ) into the wcpt_search__query function.
// Build the query
$exact_query = $base_query .
"WHERE " . $fixed_conditions .
"AND (
" . end($conditions_parts) . " = '$esc_keyword'
OR " . end($conditions_parts) . " LIKE '% $esc_keyword %'
OR " . end($conditions_parts) . " LIKE '$esc_keyword %'
OR " . end($conditions_parts) . " LIKE '% $esc_keyword'
)";
-
The Flaw: The function uses
end($conditions_parts)to retrieve the column name. Because this is concatenated directly using the . operator, any manipulation of the preceding query logic or the keyword itself can break out of the intended structure. -
The “Sink”: The actual execution happens at
$wpdb->get_col($exact_query). Since$exact_queryis a raw string built via concatenation, the SQL engine executes whatever is injected before it reaches the closing parenthesis.
Patch Analysis: Transition to Parameterization The patch introduced in version 4.6.3 completely removes the string concatenation.
Before:

After:
The developer now uses $wpdb->prepare(). By utilizing placeholders (like %s for strings), WordPress handles the quoting and escaping at the database driver level, making it impossible for a user-supplied string to be interpreted as a SQL command.

Root Cause Analysis: Tracing from source to sink
Source (Entry Point)
The vulnerability begins in the wcpt_search() function, which serves as the primary entry point for processing search queries. The Source is the $filter_info['keyword'] array element, which receives unsanitized user input via a HTTP request (typically through parameters like [ID]_search_1 or filter_info[keyword]).
- Data Entry: The raw input is stored in
$keyword_phrase. - Initial Processing: The input is converted to lowercase and trimmed, then split into an array of $keywords based on a separator.
function wcpt_search($filter_info, $post__in = array())
{
global $wpdb;
if (empty($filter_info['keyword'])) {
return $post__in;
}
$filter_info = apply_filters('wcpt_search_args', $filter_info);
if (empty($filter_info['post_type'])) {
$filter_info['post_type'] = 'product';
}
if (!empty($filter_info['use_default_search'])) {
$search_terms = array_map('trim', explode(' ', $filter_info['keyword']));
$sql = array();
Data Transformation (Vulnerable Flow)
The execution flow moves to the wcpt_search__query() function. The plugin attempts to build a complex dynamic SQL query by manually parsing and reassembling strings:
The function uses explode('WHERE', $query, 2) to isolate the base query.
It then uses explode(‘AND’, …) to isolate conditions.
if ($permitted['keyword_exact']) {
// Extract the base part of the query (everything before WHERE)
$query_parts = explode('WHERE', $query, 2);
$base_query = $query_parts[0];
// Extract the fixed conditions (everything before the last AND)
$conditions_parts = explode('AND', $query_parts[1]);
$fixed_conditions = implode('AND', array_slice($conditions_parts, 0, -1));
The user-controlled $keyword is passed through $wpdb->esc_like($keyword).
if ($permitted['phrase_like']) {
$esc_keyword_phrase = $wpdb->esc_like($keyword_phrase);
$post_ids = apply_filters('wcpt_search__query_results', $wpdb->get_col($query . " LIKE '%$esc_keyword_phrase%'"));
$location['phrase_like'] = $post_ids;
foreach ($wcpt_search__keyword_product_matches as $_keyword => $post_ids) {
$wcpt_search__keyword_product_matches[$_keyword] = array_merge($wcpt_search__keyword_product_matches[$_keyword], $post_ids);
}
}
foreach ($keywords as $k => $keyword) {
$esc_keyword = $wpdb->esc_like($keyword);
// keyword exact
if ($permitted['keyword_exact']) {
// Extract the base part of the query (everything before WHERE)
$query_parts = explode('WHERE', $query, 2);
$base_query = $query_parts[0];
// Extract the fixed conditions (everything before the last AND)
$conditions_parts = explode('AND', $query_parts[1]);
$fixed_conditions = implode('AND', array_slice($conditions_parts, 0, -1));
Note: While esc_like() escapes characters like % and _, it does not escape single quotes (') used for string termination in SQL.
Sink (Execution Point)
The Sink is located within wcpt_search__query() where the $exact_queryis executed by the database:
$exact_query = $base_query .
"WHERE " . $fixed_conditions .
"AND (
" . end($conditions_parts) . " = '$esc_keyword'
OR " . end($conditions_parts) . " LIKE '% $esc_keyword %'
OR " . end($conditions_parts) . " LIKE '$esc_keyword %'
OR " . end($conditions_parts) . " LIKE '% $esc_keyword'
)";
$post_ids = apply_filters('wcpt_search__query_results', $wpdb->get_col($exact_query));
$location['keyword_exact'] = array_merge($location['keyword_exact'], $post_ids);
$wcpt_search__keyword_product_matches[$keyword] = array_merge($wcpt_search__keyword_product_matches[$keyword], $post_ids);
}
Proof Of Concept (POC)
Baseline Request
Initially, a standard search request with the parameter 123 was sent. The server responded in approximately 1 second, which is the expected latency for a normal database query.

Time-Based Injection Attack
To confirm the SQL Injection vulnerability, I injected a time-delay payload into the search parameter. Due to the plugin’s architecture—which iterates search keywords across multiple database columns (title, content, SKU, etc.),the SLEEP() command is executed multiple times within a single request. This creates a Multiplier Effect.
Test Case 1: Injecting 123' OR SLEEP(0.1) -- -
- Result: The server response time jumped to 23 seconds.
- Analysis: This indicates the
SLEEP(0.1)command was executed roughly 230 times during the search process across different fields and match types.

Test Case 2: Injecting 123' OR SLEEP(0.2) -- -
- Result: The server response time doubled to approximately 45 seconds.

Conclusion
The linear correlation between the SLEEP() duration and the total response time confirms that the input is being concatenated directly into the SQL query without proper sanitization or parameterization. This allows an unauthenticated attacker to execute arbitrary SQL commands, potentially leading to full database compromise.