WP Table Builder <= 2.0.19 - Incorrect Authorization to Authenticated (Subscriber+) Arbitrary Table Creation
Description
The WP Table Builder – Drag & Drop Table Builder plugin for WordPress is vulnerable to unauthorized modification of data due to an incorrect authorization check on the save_table() function in all versions up to, and including, 2.0.19. This makes it possible for authenticated attackers, with Subscriber-level access and above, to create new wptb-table posts.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:NTechnical Details
<=2.0.19What Changed in the Fix
Changes introduced in v2.0.20
Source Code
WordPress.org SVN# Research Plan: CVE-2025-13753 - Unauthorized Table Creation in WP Table Builder ## Vulnerability Summary The **WP Table Builder** plugin (versions <= 2.0.19) contains an incorrect authorization vulnerability in its `save_table()` AJAX handler. The function `WP_Table_Builder\Inc\Admin\Admin_Menu::…
Show full research plan
Research Plan: CVE-2025-13753 - Unauthorized Table Creation in WP Table Builder
Vulnerability Summary
The WP Table Builder plugin (versions <= 2.0.19) contains an incorrect authorization vulnerability in its save_table() AJAX handler. The function WP_Table_Builder\Inc\Admin\Admin_Menu::save_table() validates authorization using an "OR" logic between two conditions: one that checks for a specific capability (Settings_Manager::ALLOWED_ROLE_META_CAP) and a nonce, and another that checks only a nonce (wptb-import-security-nonce).
Because the second condition lacks a capability check (current_user_can), any authenticated user (including Subscribers) who can obtain the wptb-import-security-nonce can trigger the insert_table_to_db() function to create new wptb-tables posts.
Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php?action=save_table - Method: POST
- Authentication: Authenticated (Subscriber level or higher)
- Vulnerable Parameter: The request body must be a JSON object containing
security_code(the nonce) andcontent. - Precondition: The attacker must be logged in as a Subscriber and extract the
wptb-import-security-nonce.
Code Flow
- Entry Point: The AJAX action
wp_ajax_save_tableis registered inInc\Admin\Admin_Menu::__construct()and points tosave_table(). - Input Handling:
save_table()reads the raw request body usingfile_get_contents('php://input')and decodes it viajson_decode. - Vulnerable Authorization Check:
If$verified = current_user_can(Settings_Manager::ALLOWED_ROLE_META_CAP) && wp_verify_nonce($params->security_code, 'wptb-security-nonce'); $import_verified = wp_verify_nonce($params->security_code, 'wptb-import-security-nonce'); if (!$verified && !$import_verified) { wp_die(json_encode(['security_problem', ''])); }$import_verifiedis true, the check passes regardless of the user's capabilities. - Logic Path:
- The code checks if
$params->idexists and is a valid post. - If
!property_exists($params, 'id'), it proceeds to$this->insert_table_to_db($params).
- The code checks if
- Sink:
insert_table_to_db()callswp_insert_post()with'post_type' => 'wptb-tables', creating a new table in the database.
Nonce Acquisition Strategy
The wptb-import-security-nonce is used by the plugin's import functionality. To obtain it:
- Identify the Script: The plugin localizes data for its admin scripts. Based on the source,
Admin_Menuenqueues scripts that likely contain these nonces. - Access Admin: Subscribers can access the WordPress dashboard (
/wp-admin/profile.php). - Extraction:
- Navigate to
/wp-admin/(or any admin page available to a Subscriber). - The plugin likely uses
wp_localize_scriptto export nonces. Based on the plugin structure, check for a global JavaScript object. - Target Variable: Look for
wptb_admin_objorwptb_vars(inferred from plugin conventions). - Nonce Key: The key is likely
import_nonceorwptb_import_nonce. - Browser Execution:
// Example extraction via browser_eval window.wptb_admin_obj?.import_nonce || window.wptb_vars?.import_nonce
- Navigate to
Exploitation Strategy
- Log in: Use
http_requestto authenticate as a Subscriber. - Extract Nonce:
- Use
browser_navigateto go to/wp-admin/. - Use
browser_evalto search for the nonce. Search specifically for the string"wptb-import-security-nonce"in the page source if the variable name is unclear, or iterate through localized objects.
- Use
- Craft Payload:
- Prepare a JSON body.
security_code: The extracted nonce.title: "Exploit Table".content: A JSON-encoded string representing a table structure (e.g.,{"cells":[]}). Note: The code callsTableRenderer::render(json_decode($params->content, true)), socontentmust be a double-encoded JSON string or a valid JSON string that decodes to an array.
- Execute Attack:
- Use
http_requestto send a POST toadmin-ajax.php?action=save_table. - Headers:
Content-Type: application/json. - Body: The prepared JSON payload.
- Use
Test Data Setup
- User Creation: Create a user with the
subscriberrole.wp user create attacker attacker@example.com --role=subscriber --user_pass=password
- Plugin Activation: Ensure
wp-table-builderis active.
Expected Results
- The HTTP response should be a JSON array:
["saved", <POST_ID>]. - A new post of type
wptb-tablesshould exist in the database with the title "Exploit Table".
Verification Steps
- Check Database via CLI:
Confirm "Exploit Table" appears in the list.wp post list --post_type=wptb-tables --field=post_title - Check Post Existence:
wp post exists <POST_ID_FROM_RESPONSE>
Alternative Approaches
- If Subscriber access to admin is restricted: Check if the plugin enqueues the import nonce on the frontend when certain shortcodes are present.
- Shortcode Search:
grep -r "add_shortcode" .to find elements that might trigger script loading. - Nonce Bypass via Action Mismatch: If
wptb-import-security-nonceis not found, check ifwp_verify_nonce($params->security_code, -1)(the default action) is ever triggered or if the nonce check is absent in other AJAX actions likewp_ajax_get_table. (However, the primary vulnerability identified is the OR logic insave_table).
Summary
The WP Table Builder plugin is vulnerable to unauthorized table creation due to flawed authorization logic in the `save_table()` AJAX handler. Authenticated users, including those with Subscriber-level permissions, can bypass capability checks by using a valid import nonce, allowing them to create new 'wptb-tables' posts via a crafted AJAX request.
Vulnerable Code
// inc/admin/class-admin-menu.php line 69 public function save_table() { $params = json_decode(file_get_contents('php://input')); $verified = current_user_can(Settings_Manager::ALLOWED_ROLE_META_CAP) && wp_verify_nonce($params->security_code, 'wptb-security-nonce'); $import_verified = wp_verify_nonce($params->security_code, 'wptb-import-security-nonce'); if (!$verified && !$import_verified) { wp_die(json_encode(['security_problem', ''])); } try { $params->content = TableRenderer::render(json_decode($params->content, true), $params->id ?? 'startedid-0'); } catch (\Exception $e) { wp_die(json_encode(['error', $e->getMessage()])); } if (!property_exists($params, 'id') || !absint($params->id) || get_post_status(absint($params->id)) != 'draft') { $id = $this->insert_table_to_db($params); wp_die(json_encode(['saved', $id])); }
Security Fix
@@ -68,9 +68,14 @@ public function save_table() { + + if (!\WPTableBuilder\Admin\Authorization::can_edit()) { + wp_die(json_encode(['security_problem', ''])); + } + $params = json_decode(file_get_contents('php://input')); - $verified = current_user_can(Settings_Manager::ALLOWED_ROLE_META_CAP) && wp_verify_nonce($params->security_code, 'wptb-security-nonce'); + $verified = wp_verify_nonce($params->security_code, 'wptb-security-nonce'); $import_verified = wp_verify_nonce($params->security_code, 'wptb-import-security-nonce'); if (!$verified && !$import_verified) {
Exploit Outline
To exploit this vulnerability, an attacker first authenticates as a Subscriber and navigates to any admin page (like `/wp-admin/profile.php`) to extract the `wptb-import-security-nonce` from localized JavaScript objects (such as `wptb_admin_obj`). The attacker then constructs a POST request to `/wp-admin/admin-ajax.php?action=save_table` with a JSON-encoded body. This body includes the extracted nonce as the `security_code` and a table structure in the `content` parameter. Because the server-side check allows authorization if the import nonce is valid—without verifying if the user has the required capabilities to edit tables—the plugin processes the request and creates a new table post in the database.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.