Ninja Tables – Easy Data Table Builder <= 5.2.5 - Authenticated (Contributor+) Information Exposure
Description
The Ninja Tables – Easy Data Table Builder plugin for WordPress is vulnerable to Sensitive Information Exposure in all versions up to, and including, 5.2.5. This makes it possible for authenticated attackers, with Contributor-level access and above, to extract sensitive user or configuration data.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:NTechnical Details
What Changed in the Fix
Changes introduced in v5.2.6
Source Code
WordPress.org SVNThis research plan targets **CVE-2026-25008**, an information exposure vulnerability in **Ninja Tables**. The vulnerability exists because the plugin's custom REST API router (defined in `app/Http/Routes/routes.php`) uses a `UserPolicy` that likely only verifies a user is authenticated, failing to e…
Show full research plan
This research plan targets CVE-2026-25008, an information exposure vulnerability in Ninja Tables. The vulnerability exists because the plugin's custom REST API router (defined in app/Http/Routes/routes.php) uses a UserPolicy that likely only verifies a user is authenticated, failing to enforce high-level administrative capabilities (like manage_options) for sensitive data retrieval. This allows Contributor-level users to access table configurations and, more critically, the actual data rows stored within those tables.
1. Vulnerability Summary
- Vulnerability: Authenticated (Contributor+) Information Exposure.
- Location: Custom REST API endpoints registered in
app/Http/Routes/routes.phpand handled byTablesControllerandTableItemsController. - Cause: Insufficient authorization checks within the
UserPolicyor the controller methods. While the Admin UI is restricted vianinja_table_admin_role()inAdminMenuHandler.php, the underlying REST API allows any logged-in user to query table lists and row data.
2. Attack Vector Analysis
- Endpoint:
/wp-json/ninja-tables/v1/tablesand/wp-json/ninja-tables/v1/tables/{id}/item - HTTP Method:
GET - Authentication: Authenticated (Contributor level or higher).
- Precondition: At least one table must exist in the system (created by an Administrator).
- Required Headers:
X-WP-Nonce(standard WordPress REST API nonce).
3. Code Flow
- Route Registration: In
app/Http/Routes/routes.php, routes are grouped under$router->withPolicy('UserPolicy').$route->get('/', [TablesController::class, 'index'])(Prefix:tables)$route->get('/', [TableItemsController::class, 'index'])(Prefix:tables/{id}/item)
- Request Handling: When a Contributor sends a request to
/wp-json/ninja-tables/v1/tables, theUserPolicyis evaluated. If it only checksis_user_logged_in(), the request proceeds. - Data Retrieval:
TablesController::index()callsPost::getPosts(), which queries theninja-tablepost type without ownership restrictions.TableItemsController::index()(implied logic) queries theninja_table_itemsdatabase table for the given table ID and returns all rows.
- Sink: The data is returned as a JSON response via
$this->json($data, 200)in the controller.
4. Nonce Acquisition Strategy
The plugin uses the standard WordPress REST API nonce (wp_rest). Any authenticated user, including a Contributor, can retrieve this nonce from the default WordPress admin dashboard.
- Login: Log in as the Contributor user using
browser_login. - Navigate: Navigate to the WordPress Profile page (
/wp-admin/profile.php). - Extract: Execute JavaScript to retrieve the nonce from the global
wpApiSettingsobject provided by WordPress core.- Command:
browser_eval("wpApiSettings.nonce")
- Command:
- Fallback: If
wpApiSettingsis not available, the nonce can be extracted from the page source where scripts are localized (searching for"nonce":"...").
5. Exploitation Strategy
Step 1: List All Tables
Retrieve the list of all tables to find the ID of a target table containing sensitive information.
- Endpoint:
/wp-json/ninja-tables/v1/tables - Method:
GET - Headers:
Content-Type: application/jsonX-WP-Nonce: [EXTRACTED_NONCE]
- Expected Response: A JSON object containing an array of tables in the
datakey.
Step 2: Extract Table Data
Retrieve the actual rows from the target table identified in Step 1.
- Endpoint:
/wp-json/ninja-tables/v1/tables/{TABLE_ID}/item - Method:
GET - Headers:
Content-Type: application/jsonX-WP-Nonce: [EXTRACTED_NONCE]
- Expected Response: A JSON object containing the values of every row in the table, potentially exposing PII or internal configurations.
Step 3: Leak User Information (Optional/Alternative)
Check if author information is exposed.
- Endpoint:
/wp-json/ninja-tables/v1/wp-posts/authors - Method:
GET - Headers:
X-WP-Nonce: [EXTRACTED_NONCE]
6. Test Data Setup
- Administrator Action:
- Create a table named "Sensitive Customer Data".
- Add columns: "Name", "Email", "Phone".
- Insert a row:
{"Name": "John Doe", "Email": "john@secret.com", "Phone": "555-0199"}.
- Attacker Action:
- Create a user with the Contributor role.
7. Expected Results
- The Contributor user, who should not have access to Ninja Tables data, successfully receives a
200 OKresponse from the REST endpoints. - The JSON response contains the private table's title, settings, and row values (e.g.,
john@secret.com).
8. Verification Steps
- Verify via WP-CLI: Check the database directly to confirm the table ID and content match the leaked data.
wp post list --post_type=ninja-tablewp db query "SELECT * FROM wp_ninja_table_items WHERE table_id = [ID]"
- Check Permissions: Confirm the Contributor user lacks the
manage_optionscapability.wp user cap list [USER_ID]
9. Alternative Approaches
If the ninja-tables/v1 namespace is different in the specific version:
- Check
app/Hooks/Handlers/AdminMenuHandler.phpinsidegetRestInfo():$ns = $app->config->get('app.rest_namespace'); // Check config/app.php or equivalent $ver = $app->config->get('app.rest_version'); - Search for
register_rest_routecalls in the entire codebase if the custom router is not used for all versions. - Test the shortcode-based info exposure in
PublicDataHandler::tableInfoShortcodeby creating a post as Contributor and inserting[ninja_table_info id="[ID]" field="last_editor"]to see if it bypasses theis_preview()check.
Summary
The Ninja Tables plugin for WordPress is vulnerable to information exposure because its REST API and internal shortcode handlers lack adequate authorization checks beyond basic authentication. Authenticated users with Contributor-level access or higher can exploit this to list all table configurations and extract sensitive row data, including PII or internal configurations.
Vulnerable Code
// app/Http/Routes/routes.php $router->withPolicy('UserPolicy')->group(function ($router) { $router->prefix('tables')->group(function ($route) { $route->get('/', [TablesController::class, 'index']); // ... $route->prefix('/{id}')->group(function ($route) { // ... $route->prefix('/item')->group(function ($route) { $route->get('/', [TableItemsController::class, 'index'])->int('id'); --- // app/Http/Controllers/TablesController.php, lines 25-33 $args = array( 'posts_per_page' => $perPage, 'offset' => $skip, 'orderby' => Sanitizer::sanitizeTextField(Arr::get($request->all(), 'orderBy')), 'order' => Sanitizer::sanitizeTextField(Arr::get($request->all(), 'order')), 'post_type' => $this->cptName, 'post_status' => 'any', ); --- // app/Hooks/Handlers/PublicDataHandler.php, lines 105-110 $id = absint($id); $table = get_post($id); if (!$table) { return; }
Security Fix
@@ -290,7 +290,9 @@ $cptName = 'ninja-table'; $tableCount = wp_count_posts($cptName); $totalPublishedTable = 0; + $totalTrashedTable = 0; $publish = property_exists($tableCount, "publish") ? $tableCount->publish : 0; + $trash = property_exists($tableCount, "trash") ? $tableCount->trash : 0; if ($tableCount && $publish > 1) { $leadStatus = $app->applyFilters('ninja_tables_show_lead', $leadStatus); @@ -304,6 +306,10 @@ $totalPublishedTable = $publish; } + if ($tableCount && $trash > 0) { + $totalTrashedTable = $trash; + } + $hasFluentFrom = defined('FLUENTFORM_VERSION'); $isFluentFromUpdated = false; $hasTablePress = defined('TABLEPRESS_ABSPATH'); @@ -363,6 +369,7 @@ 'hasValidLicense' => get_option('_ninjatables_pro_license_status'), 'i18n' => I18nStrings::getStrings(), 'published_tables' => $totalPublishedTable, + 'trashed_tables' => $totalTrashedTable, 'preview_required_scripts' => array( $assets . "css/ninjatables-public.css", $assets . "libs/footable/js/footable.min.js", @@ -86,8 +86,9 @@ $id = absint($id); $table = get_post($id); - if (!$table) { - return; + + if (!$table || $table->post_type !== 'ninja-table' || $table->post_status !== 'publish') { + return 'Error: Invalid table ID.'; } $validFields = [ @@ -169,6 +170,11 @@ } $id = absint($id); + + if (get_post_type($id) !== 'ninja-table' || get_post_status($id) !== 'publish') { + return 'Sorry! The table is not available.'; + } + $tableSettings = ninja_table_get_table_settings($id, 'public'); if ($row_id) { @@ -281,7 +287,7 @@ } $table = get_post($table_id); - if (!$table || $table->post_type != 'ninja-table') { + if (!$table || $table->post_type !== 'ninja-table' || $table->post_status !== 'publish') { return; } @@ -362,7 +368,7 @@ $table = get_post($table_id); - if (!$table || $table->post_type != 'ninja-table') { + if (!$table || $table->post_type !== 'ninja-table' || $table->post_status !== 'publish') { return; } @@ -50,10 +52,10 @@ public function store(Request $request) { - if ( ! Sanitizer::sanitizeTextField(Arr::get($request->all(), 'post_title'))) { - $this->sendError(array( - 'message' => __('The name field is required.', 'ninja-tables') - ), 423); + if (empty($request->get('post_title'))) { + wp_send_json([ + 'message' => __('Title is required', 'ninja-tables') + ], 422); }
Exploit Outline
The exploit leverages the plugin's custom REST API endpoints, which are protected by a 'UserPolicy' that only checks if a user is authenticated rather than enforcing specific administrative roles. 1. Authentication: Log in to the target WordPress site with Contributor-level credentials. 2. Nonce Retrieval: Obtain a valid WordPress REST API nonce (standard `_wpnonce`) from the admin dashboard (e.g., by checking `wpApiSettings.nonce` in the browser console). 3. Table Discovery: Send a GET request to `/wp-json/ninja-tables/v1/tables` with the `X-WP-Nonce` header. The server will return a JSON list of all defined tables, regardless of the user's permissions, including table IDs and titles. 4. Data Extraction: For each discovered table ID, send a GET request to `/wp-json/ninja-tables/v1/tables/{ID}/item`. The server responds with the complete set of row data for that table, exposing any information stored within.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.