wpDataTables (Premium) <= 6.5.1.1 - Unauthenticated Stored Cross-Site Scripting
Description
The wpDataTables (Premium) plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 6.5.1.1 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:NTechnical Details
What Changed in the Fix
Changes introduced in v6.5.1.2
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-57672 (wpDataTables Stored XSS) ## 1. Vulnerability Summary The **wpDataTables (Premium)** plugin (versions <= 6.5.1.1) is vulnerable to **Unauthenticated Stored Cross-Site Scripting (XSS)**. The vulnerability exists because the plugin registers AJAX handlers …
Show full research plan
Exploitation Research Plan: CVE-2026-57672 (wpDataTables Stored XSS)
1. Vulnerability Summary
The wpDataTables (Premium) plugin (versions <= 6.5.1.1) is vulnerable to Unauthenticated Stored Cross-Site Scripting (XSS). The vulnerability exists because the plugin registers AJAX handlers for manual table editing (wp_ajax_nopriv_wpdt_save_manual_edits) that do not adequately verify user permissions or sanitize input data. An unauthenticated attacker can inject malicious JavaScript into a "Manual" table, which then executes in the context of any user (including administrators) viewing the table on the frontend or backend.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
wpdt_save_manual_edits(viawp_ajax_nopriv_hook) - Vulnerable Parameter: Input data fields (e.g., column values) sent in the
POSTrequest. - Authentication: None (Unauthenticated).
- Preconditions:
- A "Manual" table must exist (Table Type:
manual). - "Allow frontend editing" must be enabled for the table.
- The table must be published on a public page/post via shortcode.
- A "Manual" table must exist (Table Type:
3. Code Flow
- Entry Point: An unauthenticated user sends a
POSTrequest toadmin-ajax.phpwithaction=wpdt_save_manual_edits. - AJAX Hook: The request is caught by the hook registered in the plugin (usually in a main controller or
wdt_functions.php):add_action('wp_ajax_nopriv_wpdt_save_manual_edits', 'wdt_save_manual_edits'); - Processing: The handler (inferred function name
wdt_save_manual_edits) retrieves the table ID from$_POST['table_id']. - Logic Failure: The plugin checks if the table is "Editable" but fails to verify if the current unauthenticated user has the
manage_optionsor appropriate edit permissions. It relies solely on a nonce that is exposed to public users. - Storage: The raw payload in the data columns is saved into the database table defined in the schema:
{$wpdb->prefix}wpdatatables_rows(or viaupdate_post_metafor simple tables). - Sink: When a user views the table,
WPDataTable::render()(insource/class.wpdatatable.php) fetches the data. The data is echoed to the page without context-aware escaping (e.g., usingesc_htmlorwp_kses).
4. Nonce Acquisition Strategy
The wpdt_save_manual_edits action requires a WordPress nonce. This nonce is generated for unauthenticated users (UID 0) and localized into the frontend.
- Identify Shortcode: The plugin uses
[wpdatatable id=ID]to render tables. - Create Test Page:
wp post create --post_type=page --post_title="Table Page" --post_status=publish --post_content='[wpdatatable id=1]' - Navigate & Extract: Navigate to the page containing the table. The plugin enqueues a script that localizes settings into a global JS object.
- JS Variable: Based on plugin standards, the variable is
wpdtFrontendConfigorwpDataTablesL10n. - Extraction Command:
// Recommended browser_eval logic window.wpdtFrontendConfig?.nonce || window.wpDataTablesL10n?.wpdt_nonce
5. Exploitation Strategy
Step 1: Data Setup
Prepare a manual table and enable editing.
# This is usually done via the UI, but we ensure a manual table exists
# Table ID 1, Type: manual, Editable: 1
wp db query "UPDATE wp_wpdatatables SET table_type='manual', editable=1 WHERE id=1"
Step 2: Extract Nonce
Use the browser to find the nonce required for the wpdt_save_manual_edits action.
Step 3: Send Malicious Payload
Send an AJAX request to inject the XSS payload.
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Method:
POST - Content-Type:
application/x-www-form-urlencoded - Body Parameters:
action:wpdt_save_manual_editstable_id:1nonce:[EXTRACTED_NONCE]wdt_id:0(indicates a new row)values[0]:<img src=x onerror=alert(document.domain)>(assuming column index 0 is a string type)
Step 4: Trigger Execution
Navigate to the table page (or the admin "Browse Tables" page at admin.php?page=wpdatatables-administration).
6. Test Data Setup
- User: No specific user needed (unauthenticated).
- Table: A manual table with at least one string column.
- Page: A public page containing the shortcode
[wpdatatable id=1].
7. Expected Results
- The AJAX request should return a JSON response with
success: trueor a row ID. - When an admin views the table, a JavaScript alert showing the document domain should appear.
- The payload remains stored in the database, affecting all subsequent viewers.
8. Verification Steps
After performing the HTTP request, verify the injection via WP-CLI:
# Check the wp_wpdatatables_rows table for the payload
wp db query "SELECT data FROM wp_wpdatatables_rows WHERE table_id=1"
Or check if the payload exists in the rendered HTML:
# Use browser_navigate to the table page and check for the alert/payload
browser_eval "document.body.innerHTML.includes('<img src=x onerror=alert')"
9. Alternative Approaches
- Simple Tables: If the vulnerability affects "Simple Tables" (a premium feature), the entry point might be
wpdt_save_simple_tableand the payload would be stored in thecontentcolumn of thewp_wpdatatables_templatestable. - Admin-Side Trigger: If the frontend is protected, the payload can still be triggered when an admin visits the Dashboard or Browse Tables pages, as
source/class.wdtbrowsetable.phpreturnsitem[$column_name]unsanitized in thecolumn_defaultmethod.
Summary
The wpDataTables (Premium) plugin is vulnerable to unauthenticated Stored and Reflected Cross-Site Scripting (XSS) due to insufficient input sanitization and output escaping. Attackers can exploit the 'wpdt_save_manual_edits' AJAX action to store malicious scripts or use the 'wdt_search' parameter to reflect scripts that execute when users or administrators view the table.
Vulnerable Code
// source/class.wdtbrowsetable.php // Admin-side sink for stored table data case 'id': case 'title': default: return $item[$column_name]; --- // templates/frontend/table_main.inc.php (Line 22) // Reflected sink for table description data via value attribute <input type="hidden" id="<?php echo esc_attr($this->getId()) ?>_desc" value='<?php echo $this->getJsonDescription(); ?>'/> --- // source/class.wpdatatable.php (Line 2545) // Lack of sanitization for the search parameter before being passed to the description if (isset($_GET['wdt_search'])) { $this->setDefaultSearchValue($_GET['wdt_search']); }
Security Fix
@@ -2543,7 +2543,7 @@ $columnIndex = 1; // Check the search values passed from URL if (isset($_GET['wdt_search'])) { - $this->setDefaultSearchValue($_GET['wdt_search']); + $this->setDefaultSearchValue(sanitize_text_field(wp_unslash($_GET['wdt_search']))); } // Define all column-dependent rendering rules @@ -2953,7 +2953,7 @@ $obj = apply_filters('wpdatatables_filter_table_description', $obj, $this->getWpId(), $this); - return json_encode($obj, JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG); + return json_encode($obj, JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP); } @@ -19,7 +19,7 @@ ?> <?php do_action('wpdatatables_before_table', $this->getWpId()); ?> <?php wp_nonce_field('wdtFrontendEditTableNonce', 'wdtNonceFrontendEdit'); ?> - <input type="hidden" id="<?php echo esc_attr($this->getId()) ?>_desc" value='<?php echo $this->getJsonDescription(); ?>'/> + <input type="hidden" id="<?php echo esc_attr($this->getId()) ?>_desc" value='<?php echo esc_attr($this->getJsonDescription()); ?>'/>
Exploit Outline
To exploit the Stored XSS, an unauthenticated attacker identifies a page with a manual table and extracts the 'wdtNonceFrontendEdit' nonce from the localized JavaScript (e.g., wpdtFrontendConfig). They then send a POST request to '/wp-admin/admin-ajax.php' with 'action=wpdt_save_manual_edits' containing a malicious script in the column values. This script executes whenever an administrator views the table in the backend. For Reflected XSS, an attacker crafts a URL with the 'wdt_search' parameter containing a payload designed to break out of the single-quoted HTML 'value' attribute in the table's hidden description input (e.g., using a closing quote and a tag like <img onerror=alert(1)>), which executes upon page load.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.