Taskbuilder <= 5.0.3 - Authenticated (Administrator+) Stored Cross-Site Scripting via 'Block Emails' Field
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:NTechnical Details
What Changed in the Fix
Changes introduced in v5.0.4
Source Code
WordPress.org SVN# 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 fromasset/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_htmldisabled (standard for Multi-site or viadefine('DISALLOW_UNFILTERED_HTML', true);).
3. Code Flow
- Input Submission: In
asset/js/admin.js, the functionwppm_set_page_settings()is called when the settings form is saved. It gathers data from the form with ID#wppm-frm-psusingnew FormData(). - Processing: The AJAX request is sent to
admin-ajax.php. The backend handler forwppm_set_page_settings(likely in anincludes/admin/file not fully provided, but referenced by the JS) saves the input for the "Block Emails" field into the WordPress optionwppm_en_ignore_emails. - Storage: The data is stored in the
wp_optionstable viaupdate_option(). - 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 inwppm_get_page_settings()inadmin.js). - Sink: The backend handler for
wppm_get_page_setingsretrieves thewppm_en_ignore_emailsoption and echoes it directly into the HTML response without usingesc_attr()oresc_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.
- Navigate to the Taskbuilder settings page:
wp-admin/admin.php?page=wppm-settings. - Trigger the Page Settings view by clicking the "Page Settings" tab or executing the JS:
wppm_get_page_settings();. - Extract Nonce: Once the form
#wppm-frm-psis loaded into the container.wppm_setting_col2, usebrowser_evalto 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.
- The JS localization key for the plugin is
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
- Login as an Administrator.
- Navigate to the settings page to initialize the session and script context.
- Load Page Settings via AJAX to get the form.
- 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 } });
- Action:
- Trigger XSS: Navigate back to the Page Settings tab. The script will execute.
6. Test Data Setup
- Plugin Installation: Ensure Taskbuilder <= 5.0.3 is installed and active.
- Configuration: (Optional) To strictly verify the vulnerability regardless of the
unfiltered_htmlcapability, adddefine( 'DISALLOW_UNFILTERED_HTML', true );towp-config.php. - 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
- Database Check: Use WP-CLI to verify the stored option:
Confirm it contains thewp option get wppm_en_ignore_emails<script>payload. - 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 thewppm_get_page_setingsAJAX 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_requesttool 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_settingsis not the correct action, search the plugin files foradd_action( 'wp_ajax_to find all registered AJAX handlers.
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
@@ -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.