CVE-2026-28039

wpDataTables (Premium) <= 6.5.0.1 - Unauthenticated Local File Inclusion

highImproper Control of Filename for Include/Require Statement in PHP Program ('PHP Remote File Inclusion')
8.1
CVSS Score
8.1
CVSS Score
high
Severity
6.5.0.2
Patched in
15d
Time to patch

Description

The wpDataTables (Premium) plugin for WordPress is vulnerable to Local File Inclusion in versions up to, and including, 6.5.0.1. This makes it possible for unauthenticated attackers to include and execute arbitrary files on the server, allowing the execution of any PHP code in those files. This can be used to bypass access controls, obtain sensitive data, or achieve code execution in cases where images and other "safe" file types can be uploaded and included.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
Attack Vector
Network
Attack Complexity
High
Privileges Required
None
User Interaction
None
Scope
Unchanged
High
Confidentiality
High
Integrity
High
Availability

Technical Details

Affected versions<=6.5.0.1
PublishedMarch 3, 2026
Last updatedMarch 17, 2026
Affected pluginwpdatatables

What Changed in the Fix

Changes introduced in v6.5.0.2

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# CVE-2026-28039 - wpDataTables (Premium) Unauthenticated LFI Research Plan ## 1. Vulnerability Summary The **wpDataTables (Premium)** plugin is vulnerable to **Local File Inclusion (LFI)** due to the unsafe use of `include()` or `require()` statements in its data rendering engine. The vulnerabilit…

Show full research plan

CVE-2026-28039 - wpDataTables (Premium) Unauthenticated LFI Research Plan

1. Vulnerability Summary

The wpDataTables (Premium) plugin is vulnerable to Local File Inclusion (LFI) due to the unsafe use of include() or require() statements in its data rendering engine. The vulnerability exists because an unauthenticated AJAX endpoint allows users to trigger table rendering with a user-supplied configuration. By providing a configuration where the table_type is set to a type that invokes a file inclusion (likely php or serialized) and the table_content points to a local file, an attacker can execute arbitrary PHP code or read sensitive system files.

2. Attack Vector Analysis

  • Endpoint: wp-admin/admin-ajax.php
  • Action: wp_ajax_nopriv_wdt_preview_table (inferred unauthenticated action used for table construction/preview).
  • Vulnerable Parameter: table (likely a JSON-encoded string containing table settings).
  • Authentication: Unauthenticated (nopriv).
  • Preconditions: The plugin must be active. Exploitation is most effective if an attacker can first upload a "safe" file (like an image) containing PHP code to the server (e.g., via the WordPress media library or a profile picture upload).

3. Code Flow

  1. Entry Point: The unauthenticated user sends a POST request to admin-ajax.php
Research Findings
Static analysis — not yet PoC-verified

Summary

The wpDataTables (Premium) plugin is vulnerable to unauthenticated Local File Inclusion (LFI) because it dynamically constructs file paths for 'require_once' and 'include' statements using user-supplied parameters from table and chart configurations. An attacker can use path traversal sequences to include arbitrary PHP files from the server or execute code within uploaded 'safe' files like images.

Vulnerable Code

// source/class.wpdatachart.php:468
    public static function build($constructedChartData, $loadFromDB = false)
    {
        $wdtChart = 'Wdt' . ucfirst($constructedChartData['engine']) . 'Chart' . '\Wdt' . ucfirst($constructedChartData['engine']) . 'Chart';
        $chartClassFileName = 'class.' . $constructedChartData['engine'] . '.wpdatachart.php';
        require_once(WDT_ROOT_PATH . 'source/' . $chartClassFileName);
        return new $wdtChart($constructedChartData, $loadFromDB);
    }

---

// source/class.wpdatacolumn.php:673
        $columnObj = ucfirst($wdtColumnType) . 'WDTColumn';
        $columnFormatterFileName = 'class.' . strtolower($wdtColumnType) . '.wpdatacolumn.php';
        require_once($columnFormatterFileName);
        return new $columnObj($properties);

---

// source/class.wpdatatable.php:534
    public function setInterfaceLanguage($lang)
    {
        if (empty($lang)) {
            throw new WDTException('Incorrect language parameter!');
        }
        if (!file_exists(WDT_ROOT_PATH . 'source/lang/' . $lang)) {
            throw new WDTException('Language file not found');
        }
        $this->_interfaceLanguage = WDT_ROOT_PATH . 'source/lang/' . $lang;
    }

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.1/config/config.inc.php /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.2/config/config.inc.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.1/config/config.inc.php	2026-03-02 07:21:50.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.2/config/config.inc.php	2026-03-05 12:37:02.000000000 +0000
@@ -9,7 +9,7 @@
 
 // Current version
 
-define('WDT_CURRENT_VERSION', '6.5.0.1');
+define('WDT_CURRENT_VERSION', '6.5.0.2');
 
 // Version when hooks are updated
 define('WDT_INITIAL_LITE_VERSION', '3.4.2.16');
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.1/readme.txt /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.2/readme.txt
--- /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.1/readme.txt	2026-03-02 07:21:50.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.2/readme.txt	2026-03-05 12:37:02.000000000 +0000
@@ -6,7 +6,7 @@
 Requires at least: 4.0
 Tested up to: 6.9.1
 Requires PHP: 7.4
-Stable tag: 6.5.0.1
+Stable tag: 6.5.0.2
 License: GPLv2 or later
 License URI: http://www.gnu.org/licenses/gpl-2.0.html
 
@@ -424,8 +424,7 @@
 This can be changed from the “Number format” drop-down menu in the wpDataTables Settings page.
 
 = Where do I report security bugs found in this plugin? =
-
-Please report security bugs found in the source code of the wpDataTables plugin through the [Patchstack Vulnerability Disclosure Program](https://patchstack.com/database/vdp/9e5fb453-4d67-4ae9-8d4b-2aeOblec5589). The Patchstack team will assist you with verification, CVE assignment, and notify the developers of this plugin.
+Please report security bugs found in the source code of the wpDataTables plugin through the [Patchstack Vulnerability Disclosure Program](https://patchstack.com/database/vdp/9e5fb453-4d67-4ae9-8d4b-2ae0b1ec5589). The Patchstack team will assist you with verification, CVE assignment, and notify the developers of this plugin.
 
 == Screenshots ==
 1. New simple editor
@@ -443,6 +442,10 @@
 13. Google chart settings
 
 == Changelog ==
+= 6.5.0.2 =
+* Fixed vulnerability issue with Local File Inclusion (LFI).
+Other small bug fixes and stability improvements.
+
 = 6.5.0.1 =
 * Added wpDataTables capabilities for viewing tables and charts.
 Other small bug fixes and stability improvements.
@@ -1301,4 +1304,4 @@
 * Numerous bugfixes
 
 = 1.0 =
-* Initial version for wp.org
\ No newline at end of file
+* Initial version for wp.org
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.1/source/class.wdtsettingscontroller.php /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.2/source/class.wdtsettingscontroller.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.1/source/class.wdtsettingscontroller.php	2026-03-02 07:21:50.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.2/source/class.wdtsettingscontroller.php	2026-03-05 12:37:02.000000000 +0000
@@ -22,6 +22,23 @@
                 } else {
                     $setting = sanitize_textarea_field($setting);
                 }
+            } elseif ($key === 'wdtInterfaceLanguage') {
+                // Security Fix: Prevent Path Traversal / LFI for language file (CVE-2026-28039)
+                if (!empty($setting)) {
+                    // Only allow basename (no directory traversal)
+                    $setting = basename(sanitize_text_field($setting));
+
+                    // Verify it's a valid language file
+                    if (substr($setting, -8) !== '.inc.php') {
+                        $setting = ''; // Invalid format, reject it
+                    } else {
+                        // Double-check the file exists in the lang directory
+                        $langPath = WDT_ROOT_PATH . 'source/lang/' . $setting;
+                        if (!file_exists($langPath) || !is_file($langPath)) {
+                            $setting = ''; // File doesn't exist, reject it
+                        }
+                    }
+                }
             } else{
 				$setting = sanitize_text_field( $setting );
 			}
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.1/source/class.wpdatachart.php /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.2/source/class.wpdatachart.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.1/source/class.wpdatachart.php	2026-03-02 07:21:50.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.2/source/class.wpdatachart.php	2026-03-05 12:37:02.000000000 +0000
@@ -465,8 +465,18 @@
      */
     public static function build($constructedChartData, $loadFromDB = false)
     {
-        $wdtChart = 'Wdt' . ucfirst($constructedChartData['engine']) . 'Chart' . '\Wdt' . ucfirst($constructedChartData['engine']) . 'Chart';
-        $chartClassFileName = 'class.' . $constructedChartData['engine'] . '.wpdatachart.php';
+        // Security Fix: Whitelist valid chart engines to prevent LFI
+        $validEngines = array('google', 'chartjs', 'highcharts', 'apexcharts');
+
+        $engine = isset($constructedChartData['engine']) ? strtolower(sanitize_text_field($constructedChartData['engine'])) : 'google';
+
+        if (!in_array($engine, $validEngines, true)) {
+            // Invalid engine, default to google
+            $engine = 'google';
+        }
+
+        $wdtChart = 'Wdt' . ucfirst($engine) . 'Chart' . '\Wdt' . ucfirst($engine) . 'Chart';
+        $chartClassFileName = 'class.' . $engine . '.wpdatachart.php';
         require_once(WDT_ROOT_PATH . 'source/' . $chartClassFileName);
         return new $wdtChart($constructedChartData, $loadFromDB);
     }
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.1/source/class.wpdatacolumn.php /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.2/source/class.wpdatacolumn.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.1/source/class.wpdatacolumn.php	2026-03-02 07:21:50.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.2/source/class.wpdatacolumn.php	2026-03-05 12:37:02.000000000 +0000
@@ -670,8 +670,23 @@
         if (!$wdtColumnType) {
             $wdtColumnType = 'string';
         }
+
+        // Security Fix: Whitelist valid column types to prevent LFI
+        $validColumnTypes = array(
+            'string', 'int', 'float', 'date', 'datetime', 'time', 'link', 'email',
+            'image', 'file', 'formula', 'masterdetail', 'attachment'
+        );
+
+        // Normalize and validate column type
+        $wdtColumnType = strtolower(sanitize_text_field($wdtColumnType));
+
+        if (!in_array($wdtColumnType, $validColumnTypes, true)) {
+            // Invalid column type, default to string
+            $wdtColumnType = 'string';
+        }
+
         $columnObj = ucfirst($wdtColumnType) . 'WDTColumn';
-        $columnFormatterFileName = 'class.' . strtolower($wdtColumnType) . '.wpdatacolumn.php';
+        $columnFormatterFileName = 'class.' . $wdtColumnType . '.wpdatacolumn.php';
         require_once($columnFormatterFileName);
         return new $columnObj($properties);
     }
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.1/source/class.wpdatatablecache.php /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.2/source/class.wpdatatablecache.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.1/source/class.wpdatatablecache.php	2026-03-02 07:21:50.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.2/source/class.wpdatatablecache.php	2026-03-05 12:37:02.000000000 +0000
@@ -327,6 +327,17 @@
 					$format = substr(strrchr($source, "."), 1);
 					$objReader = WPDataTable::createObjectReader($source);
                 if (isset($tableData) && $tableData->file_location == 'wp_any_url'){
+                    // Security Fix: Validate URL before file_get_contents to prevent SSRF/LFI
+                    if (!filter_var($source, FILTER_VALIDATE_URL)) {
+                        throw new Exception('Invalid URL format!');
+                    }
+
+                    // Prevent access to local files via file:// protocol
+                    $parsedUrl = parse_url($source);
+                    if (!isset($parsedUrl['scheme']) || !in_array(strtolower($parsedUrl['scheme']), array('http', 'https'), true)) {
+                        throw new Exception('Only HTTP and HTTPS protocols are allowed!');
+                    }
+
                     $file = @file_get_contents($source);
                     if ($file === false){
                         throw new Exception('There is an error opening the file!');
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.1/source/class.wpdatatable.php /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.2/source/class.wpdatatable.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.1/source/class.wpdatatable.php	2026-03-02 07:21:50.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpdatatables/6.5.0.2/source/class.wpdatatable.php	2026-03-05 12:37:02.000000000 +0000
@@ -525,15 +525,40 @@
         return $this->_verticalScroll;
     }
 
+    /**
+     * @throws WDTException
+     */
     public function setInterfaceLanguage($lang)
     {        if (empty($lang)) {
             throw new WDTException('Incorrect language parameter!');
         }
-        if (!file_exists(WDT_ROOT_PATH . 'source/lang/' . $lang)) {
+
+        // Security Fix: Prevent Path Traversal / LFI attacks (CVE-2026-28039)
+        // Remove any path traversal attempts and allow only valid filenames
+        $lang = basename($lang);
+
+        // Additional security: Only allow .inc.php extension
+        if (substr($lang, -8) !== '.inc.php') {
+            throw new WDTException('Invalid language file format!');
+        }
+
+        // Build the safe path
+        $safePath = WDT_ROOT_PATH . 'source/lang/' . $lang;
+
+        // Verify the resolved path is still within the lang directory
+        $realPath = realpath($safePath);
+        $realLangDir = realpath(WDT_ROOT_PATH . 'source/lang/');
+
+        if ($realPath === false || strpos($realPath, $realLangDir) !== 0) {
+            throw new WDTException('Language file not found or path traversal detected!');
+        }
+
+        if (!file_exists($safePath)) {
             throw new WDTException('Language file not found');
         }
-        $this->_interfaceLanguage = WDT_ROOT_PATH . 'source/lang/' . $lang;
+        $this->_interfaceLanguage = $safePath;
     }
 
     public function getInterfaceLanguage()

Exploit Outline

The exploit targets the unauthenticated AJAX endpoint `wp-admin/admin-ajax.php` using actions such as `wdt_preview_table`. An attacker sends a POST request containing a JSON-encoded `table` configuration object. By manipulating properties within this object that are later passed to dynamic file loading methods—such as the `engine` key in a chart configuration (processed by `WPDataChart::build`) or the `wdtColumnType` key in a table configuration (processed by `WDTColumn::generateComponent`)—the attacker can specify arbitrary local paths. A payload like `{"engine": "../../../../wp-content/uploads/malicious"}` would cause the server to execute `require_once` on the specified path. This is especially dangerous if the attacker can upload a file (e.g., a profile picture) containing PHP code.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.