CVE-2026-2289

Taskbuilder <= 5.0.3 - Authenticated (Administrator+) Stored Cross-Site Scripting via 'Block Emails' Field

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
4.4
CVSS Score
4.4
CVSS Score
medium
Severity
5.0.4
Patched in
1d
Time to patch

Description

The Taskbuilder plugin for WordPress is vulnerable to Stored Cross-Site Scripting via admin settings in all versions up to, and including, 5.0.3 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with administrator-level permissions and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page. This only affects multi-site installations and installations where unfiltered_html has been disabled.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=5.0.3
PublishedMarch 3, 2026
Last updatedMarch 4, 2026
Affected plugintaskbuilder

What Changed in the Fix

Changes introduced in v5.0.4

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-2289 (Taskbuilder Stored XSS) ## 1. Vulnerability Summary The **Taskbuilder** plugin (<= 5.0.3) is vulnerable to **Authenticated Stored Cross-Site Scripting (XSS)** via the 'Block Emails' field in the plugin settings. The vulnerability exists because the plug…

Show full research plan

Exploitation Research Plan - CVE-2026-2289 (Taskbuilder Stored XSS)

1. Vulnerability Summary

The Taskbuilder plugin (<= 5.0.3) is vulnerable to Authenticated Stored Cross-Site Scripting (XSS) via the 'Block Emails' field in the plugin settings. The vulnerability exists because the plugin saves the input for ignored email addresses without proper sanitization and subsequently outputs it in the admin settings interface without adequate escaping. This allows an administrator (or a user with similar permissions) to inject arbitrary JavaScript that executes when the settings page is viewed, particularly in environments where unfiltered_html is disabled (e.g., WordPress Multi-site).

2. Attack Vector Analysis

  • Endpoint: wp-admin/admin-ajax.php
  • Action: wppm_set_page_settings (inferred from asset/js/admin.js)
  • Vulnerable Field: wppm_en_ignore_emails (The "Block Emails" setting)
  • Authentication Level: Administrator (required to access and save plugin settings)
  • Preconditions:
    • The attacker must have Administrator-level access.
    • To demonstrate the security failure, the environment should ideally have unfiltered_html disabled (standard for Multi-site or via define('DISALLOW_UNFILTERED_HTML', true);).

3. Code Flow

  1. Input Submission: In asset/js/admin.js, the function wppm_set_page_settings() is called when the settings form is saved. It gathers data from the form with ID #wppm-frm-ps using new FormData().
  2. Processing: The AJAX request is sent to admin-ajax.php. The backend handler for wppm_set_page_settings (likely in an includes/admin/ file not fully provided, but referenced by the JS) saves the input for the "Block Emails" field into the WordPress option wppm_en_ignore_emails.
  3. Storage: The data is stored in the wp_options table via update_option().
  4. Retrieval: When an administrator navigates to the settings page (specifically the "Page Settings" tab), the plugin calls the AJAX action wppm_get_page_setings (as seen in wppm_get_page_settings() in admin.js).
  5. Sink: The backend handler for wppm_get_page_setings retrieves the wppm_en_ignore_emails option and echoes it directly into the HTML response without using esc_attr() or esc_html(). The JS then injects this response into the DOM: jQuery('.wppm_setting_col2').html(response);.

4. Nonce Acquisition Strategy

The Taskbuilder settings form uses WordPress nonces for security. Since the settings are loaded dynamically via AJAX, the nonce must be extracted from the DOM after the settings view is rendered.

  1. Navigate to the Taskbuilder settings page: wp-admin/admin.php?page=wppm-settings.
  2. Trigger the Page Settings view by clicking the "Page Settings" tab or executing the JS: wppm_get_page_settings();.
  3. Extract Nonce: Once the form #wppm-frm-ps is loaded into the container .wppm_setting_col2, use browser_eval to find the nonce field.
    • The JS localization key for the plugin is wppm_admin.
    • The nonce is likely within the form as a hidden input.

Extraction JS:

// Ensure the page settings are loaded first
wppm_get_page_settings();
// Wait briefly then extract nonce from the newly injected form
setTimeout(() => {
    const nonce = jQuery('#wppm-frm-ps input[name="_ajax_nonce"]').val() || 
                  jQuery('#wppm-frm-ps input[name="wppm_nonce"]').val();
    return nonce;
}, 1000);

5. Exploitation Strategy

  1. Login as an Administrator.
  2. Navigate to the settings page to initialize the session and script context.
  3. Load Page Settings via AJAX to get the form.
  4. Submit the Payload:
    • Action: wppm_set_page_settings
    • Payload: <script>alert(document.domain)</script>
    • HTTP Request (Playwright):
      await http_request({
          url: "http://localhost:8080/wp-admin/admin-ajax.php",
          method: "POST",
          form: {
              action: "wppm_set_page_settings",
              wppm_en_ignore_emails: 'admin@test.com",><script>alert(document.domain)</script>',
              _ajax_nonce: extracted_nonce // Obtained from step 4
          }
      });
      
  5. Trigger XSS: Navigate back to the Page Settings tab. The script will execute.

6. Test Data Setup

  1. Plugin Installation: Ensure Taskbuilder <= 5.0.3 is installed and active.
  2. Configuration: (Optional) To strictly verify the vulnerability regardless of the unfiltered_html capability, add define( 'DISALLOW_UNFILTERED_HTML', true ); to wp-config.php.
  3. User: An admin user.

7. Expected Results

  • Upon saving, the AJAX response should indicate success: {"sucess_status":"1", "messege": "..."}.
  • Upon reloading the Page Settings tab, the browser should trigger an alert box showing the document domain.
  • The HTML source inside the "Block Emails" field in the settings UI should contain the raw, unescaped <script> tag.

8. Verification Steps

  1. Database Check: Use WP-CLI to verify the stored option:
    wp option get wppm_en_ignore_emails
    
    Confirm it contains the <script> payload.
  2. UI Verification: Navigate to wp-admin/admin.php?page=wppm-settings, click "Page Settings", and observe if the payload executes or is visible in the raw HTML response of the wppm_get_page_setings AJAX call.

9. Alternative Approaches

  • Payload Variation: If <script> tags are blocked by a WAF, try an attribute-based injection: admin@test.com" onfocus="alert(1)" autofocus=".
  • Direct Option Update: If the AJAX action name is different, use the http_request tool to intercept the network traffic while manually clicking "Save" in the browser to identify the correct parameter names.
  • Action Name Check: If wppm_set_page_settings is not the correct action, search the plugin files for add_action( 'wp_ajax_ to find all registered AJAX handlers.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Taskbuilder plugin for WordPress is vulnerable to Authenticated Stored Cross-Site Scripting via the 'Block Emails' field in the plugin settings. In versions up to 5.0.3, the plugin fails to sanitize this input during storage and escapes it improperly upon retrieval, allowing administrators to inject arbitrary JavaScript that executes whenever a user views the Page Settings tab, which particularly affects environments where unfiltered_html is restricted.

Vulnerable Code

// asset/js/admin.js line 52-61
function wppm_get_page_settings(){
  jQuery('.wppm_setting_pills li').removeClass('active');
  jQuery('#wppm_settings_page').addClass('active');
  jQuery('.wppm_setting_col2').html(wppm_admin.loading_html);
  var data = {
    action: 'wppm_get_page_setings'
  };
  jQuery.post(wppm_admin.ajax_url, data, function(response) {
    jQuery('.wppm_setting_col2').html(response);
  });
}

---

// asset/js/admin.js line 63-74
function wppm_set_page_settings(){
  jQuery('.wppm_submit_wait').show();
    var dataform = new FormData(jQuery('#wppm-frm-ps')[0]);
    jQuery.ajax({
      url: wppm_admin.ajax_url,
      type: 'POST',
      data: dataform,
      processData: false,
      contentType: false
  })

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/taskbuilder/5.0.3/asset/js/admin.js /home/deploy/wp-safety.org/data/plugin-versions/taskbuilder/5.0.4/asset/js/admin.js
--- /home/deploy/wp-safety.org/data/plugin-versions/taskbuilder/5.0.3/asset/js/admin.js	2026-01-02 17:29:02.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/taskbuilder/5.0.4/asset/js/admin.js	2026-02-24 17:57:02.000000000 +0000
@@ -312,7 +312,7 @@
 }
 
 function wppm_view_task_search_filter(page_no,page){
-  var task_search = jQuery("#wppm_view_task_search_filter").val();
+  var task_search = jQuery("#wppm_view_task_search_filter").val() || '';
   jQuery('#wppm_task_container').show();
   jQuery('#wppm_task_container').html(wppm_admin.loading_html);
 
@@ -477,7 +477,7 @@
 }
 
 function wppm_display_grid_view(page){
-  var task_search = jQuery("#wppm_view_task_search_filter").val();
+  var task_search = jQuery("#wppm_view_task_search_filter").val() || '';
   jQuery('#wppm_task_container').show();
   jQuery('#wppm_task_container').html(wppm_admin.loading_html);
   var data = {
@@ -763,7 +763,11 @@
 
 function wppm_set_edit_task_thread(task_id,proj_id){
   var dataform = new FormData(jQuery('#frm_edit_task_thread')[0]);
-  var comment_body = tinyMCE.get('wppm_edit_task_thread_editor').getContent().trim();
+  if(wppm_admin.rich_text_editor == 1){
+    var comment_body = tinyMCE.get('wppm_edit_task_thread_editor').getContent().trim();
+  }else{
+    var comment_body = jQuery('#wppm_edit_task_thread_editor').val().trim();
+  }
   dataform.append('wppm_edit_task_thread', comment_body);
   if(proj_id==0){
     wppm_task_modal_close(); 
@@ -789,7 +793,11 @@
 
 function wppm_set_edit_proj_thread(proj_id){
   var dataform = new FormData(jQuery('#frm_edit_proj_thread')[0]);
-  var comment_body = tinyMCE.get('wppm_edit_proj_thread_editor').getContent().trim();
+  if(wppm_admin.rich_text_editor == 1){
+    var comment_body = tinyMCE.get('wppm_edit_proj_thread_editor').getContent().trim();
+  }else{
+    var comment_body = jQuery('#wppm_edit_proj_thread_editor').val().trim();
+  }
   dataform.append('wppm_edit_proj_thread', comment_body);
   wppm_modal_close();
   jQuery('#wppm_task_container').html(wppm_admin.loading_html);

Exploit Outline

The exploit requires Administrator privileges. An attacker first navigates to the Taskbuilder settings page and triggers the 'Page Settings' tab to load the relevant configuration form. They then extract the AJAX nonce (wppm_nonce or _ajax_nonce) from the DOM. Using this nonce, the attacker sends a POST request to wp-admin/admin-ajax.php with the action 'wppm_set_page_settings', including a malicious script payload (e.g., <script>alert(document.domain)</script>) in the 'wppm_en_ignore_emails' parameter. The plugin saves this unvalidated value to the database. The XSS is triggered whenever any user (including other administrators) accesses the Page Settings tab, as the plugin fetches the setting via 'wppm_get_page_setings' and injects the raw, unescaped response directly into the page DOM using jQuery.html().

Check if your site is affected.

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