CVE-2015-10108: Cross-Site Request Forgery (CSRF) (CWE-352)
value: A vulnerability was found in meitar Inline Google Spreadsheet Viewer Plugin up to 0.9.6 on WordPress and classified as problematic. Affected by this issue is the function displayShortcode of the file inline-gdocs-viewer.php. The manipulation leads to cross-site request forgery. The attack may be launched remotely. Upgrading to version 0.9.6.1 is able to address this issue. The patch is identified as 2a8057df8ca30adc859cecbe5cad21ac28c5b747. It is recommended to upgrade the affected component. VDB-230234 is the identifier assigned to this vulnerability.
CWE description: The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
CVSS v3 base score: 8.8
Published: 2023-05-31T19:15Z
Repository: https://github.com/wp-plugins/inline-google-spreadsheet-viewer
Language: PHP
Files changed: inline-gdocs-viewer.php
Commit date: 2015-05-11 21:54:52 +0000
Commit message: Secure the CSV fetching code behind a WordPress nonce to avoid CSRFs.

git-svn-id: https://plugins.svn.wordpress.org/inline-google-spreadsheet-viewer/trunk@1158277 b8457f37-d9ea-0310-8a92-e5e31aec5664

Diff: diff --git a/inline-gdocs-viewer.php b/inline-gdocs-viewer.php
index e8b5e0a..c661d0d 100644
--- a/inline-gdocs-viewer.php
+++ b/inline-gdocs-viewer.php
@@ -25,10 +25,11 @@ class InlineGoogleSpreadsheetViewerPlugin {
         $this->dt_defaults = json_encode(array(
             'dom' => "TC<'clear'>lfrtip"
         ));
 
         add_action('plugins_loaded', array($this, 'registerL10n'));
+        add_action('init', array($this, 'maybeFetchGvizDataSource'));
         add_action('admin_init', array($this, 'registerSettings'));
         add_action('admin_menu', array($this, 'registerAdminMenu'));
         add_action('admin_head', array($this, 'doAdminHeadActions'));
         add_action('admin_enqueue_scripts', array($this, 'addAdminScripts'));
         add_action('admin_print_footer_scripts', array($this, 'addQuickTagButton'));
@@ -242,16 +243,14 @@ class InlineGoogleSpreadsheetViewerPlugin {
         return $url;
     }
 
     private function getGVizDataSourceUrl ($key, $query, $format) {
         $format = ($format) ? $format : 'json';
-        $url = plugins_url('vistable-proxy.php', __FILE__);
-        $url .= '?url=' . rawurlencode($key);
-        $tq = $this->sanitizeQuery($query);
-        $url .= "&tq=$tq";
-        $url .= "&tqx=out:$format";
-        return $url;
+        $base = trailingslashit(get_site_url()) . '?';
+        $qs = 'url=';
+        $qs .= rawurlencode($key .  '?tq=' . $this->sanitizeQuery($query) . "&tqx=out:$format");
+        return $base . $qs;
     }
 
     private function sanitizeQuery ($query) {
         // Due to shortcode parsing limitations of angle brackets (< and > characters),
         // manually decode only the URL encoded values for those values, which are
@@ -293,11 +292,11 @@ class InlineGoogleSpreadsheetViewerPlugin {
                 throw new Exception('[' . __('Error parsing HTTP options attribute:', 'inline-gdocs-viewer') . $e->getMessage() . ']');
             }
         }
         $resp = (empty($http_args)) ? wp_remote_get($url) : wp_remote_request($url, $http_args);
         if (is_wp_error($resp)) { // bail on error
-            throw new Exception('[' . __('Error requesting data from Google:', 'inline-gdocs-viewer') . $resp->get_error_message() . ']');
+            throw new Exception('[' . __('Error requesting data:', 'inline-gdocs-viewer') . ' ' . $resp->get_error_message() . ']');
         }
         return $resp;
     }
 
     public function parseCsv ($csv_str) {
@@ -423,10 +422,52 @@ class InlineGoogleSpreadsheetViewerPlugin {
 
     public function oEmbedHandler ($matches, $attr, $url, $rawattr) {
         return $this->displayShortcode(array('key' => $url));
     }
 
+    public function maybeFetchGvizDataSource () {
+        if (
+            !isset($_GET[$this->prefix . 'get_datasource_nonce'])
+            ||
+            !$this->isValidNonce($_GET[$this->prefix . 'get_datasource_nonce'], $this->prefix . 'get_datasource_nonce')
+        ) { return; }
+        require_once dirname(__FILE__) . '/lib/vistable.php';
+        $url = rawurldecode($_GET['url']);
+        $http_response = $this->fetchData($url, false);
+
+        $p = parse_url($url);
+        $qs = array();
+        parse_str($p['query'], $qs);
+        $tqx  = (isset($qs['tqx']))  ? $qs['tqx'] : '';
+        $tq   = (isset($qs['tq']))   ? $qs['tq']  : '';
+        $tqrt = (isset($qs['tqrt'])) ? $qs['tqrt']: '';
+        $tz   = (isset($qs['tz']))   ? $qs['tz']  : 'PDT'; // TODO: will get_option('timezone_string') work?
+        $vt = new csv_vistable($tqx, $tq, $tqrt, $tz, get_locale(), array());
+        $vt->setup_table($http_response['body']);
+        print $vt->execute();
+        exit;
+    }
+
+    private function makeNonceUrl ($url) {
+        $options = get_option($this->prefix . 'settings');
+        $options[$this->prefix . 'get_datasource_nonce'] = wp_create_nonce($this->prefix . 'get_datasource_nonce');
+        update_option($this->prefix . 'settings', $options);
+        $p = parse_url($url);
+        return $p['scheme']
+            . '://' . $p['host'] . $p['path']
+            . '?' . $p['query'] . '&'
+            . $this->prefix . 'get_datasource_nonce=' . $options[$this->prefix . 'get_datasource_nonce'];
+    }
+
+    private function isValidNonce ($nonce, $nonce_name) {
+        $options = get_option($this->prefix . 'settings');
+        $is_valid = ($nonce === $options[$nonce_name]) ? true : false;
+        unset($options[$this->prefix . 'get_datasource_nonce']);
+        update_option($this->prefix . 'settings', $options);
+        return $is_valid;
+    }
+
     /**
      * WordPress Shortcode handler.
      */
     public function displayShortcode ($atts, $content = null) {
         $x = shortcode_atts(array(
@@ -565,11 +606,11 @@ class InlineGoogleSpreadsheetViewerPlugin {
             $url = $this->getSpreadsheetUrl($x['key'], $x['gid'], $x['query']);
         } else {
             if (!empty($x['query'])) {
                 if (!empty($x['chart'])) { $fmt = 'json'; }
                 else { $fmt = 'csv'; }
-                $url = $this->getGVizDataSourceUrl($x['key'], $x['query'], $fmt);
+                $url = $this->makeNonceUrl($this->getGVizDataSourceUrl($x['key'], $x['query'], $fmt));
             }
         }
 
         if ('docsviewer' === $key_type) {
             $output = $this->getGDocsViewerOutput($x);
diff --git a/vistable-proxy.php b/vistable-proxy.php
deleted file mode 100644
index 1a61d0b..0000000
--- a/vistable-proxy.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-require_once dirname(__FILE__) . '/lib/vistable.php';
-
-$url = isset($_GET['url']) ? rawurldecode($_GET['url']) : '';
-$tqx = isset($_GET['tqx']) ? $_GET['tqx'] : '';
-$tq = isset($_GET['tq']) ? $_GET['tq'] : '';
-$tqrt = isset($_GET['tqrt']) ? $_GET['tqrt'] : '';
-$tz = isset($_GET['tz']) ? $_GET['tz'] : 'PDT';
-$locale = isset($_SERVER["HTTP_ACCEPT_LANGUAGE"]) ? $_SERVER["HTTP_ACCEPT_LANGUAGE"] : 'en_US';
-
-if (isset($_GET['locale'])) {
-    $locale = $_GET['locale'];
-}
-
-$extra = array();
-if (isset($_GET['debug']) && $_GET['debug']) {
-    header('Content-type: text/plain; charset="UTF-8"');
-    $extra['debug'] = 1;
-}
-
-$vt = new csv_vistable($tqx, $tq, $tqrt,$tz,$locale,$extra);
-
-$data = file_get_contents($url);
-
-$vt->setup_table($data);
-
-$result = $vt->execute();
-
-print $result;

Vulnerable code:         $url = plugins_url('vistable-proxy.php', __FILE__);
        $url .= '?url=' . rawurlencode($key);
        $tq = $this->sanitizeQuery($query);
        $url .= "&tq=$tq";
        $url .= "&tqx=out:$format";
        return $url;
            throw new Exception('[' . __('Error requesting data from Google:', 'inline-gdocs-viewer') . $resp->get_error_message() . ']');
                $url = $this->getGVizDataSourceUrl($x['key'], $x['query'], $fmt);
<?php
require_once dirname(__FILE__) . '/lib/vistable.php';

$url = isset($_GET['url']) ? rawurldecode($_GET['url']) : '';
$tqx = isset($_GET['tqx']) ? $_GET['tqx'] : '';
$tq = isset($_GET['tq']) ? $_GET['tq'] : '';
$tqrt = isset($_GET['tqrt']) ? $_GET['tqrt'] : '';
$tz = isset($_GET['tz']) ? $_GET['tz'] : 'PDT';
$locale = isset($_SERVER["HTTP_ACCEPT_LANGUAGE"]) ? $_SERVER["HTTP_ACCEPT_LANGUAGE"] : 'en_US';

if (isset($_GET['locale'])) {
    $locale = $_GET['locale'];
}

$extra = array();
if (isset($_GET['debug']) && $_GET['debug']) {
    header('Content-type: text/plain; charset="UTF-8"');
    $extra['debug'] = 1;
}

$vt = new csv_vistable($tqx, $tq, $tqrt,$tz,$locale,$extra);

$data = file_get_contents($url);

$vt->setup_table($data);

$result = $vt->execute();

print $result;
Fixed code:         add_action('init', array($this, 'maybeFetchGvizDataSource'));
        $base = trailingslashit(get_site_url()) . '?';
        $qs = 'url=';
        $qs .= rawurlencode($key .  '?tq=' . $this->sanitizeQuery($query) . "&tqx=out:$format");
        return $base . $qs;
            throw new Exception('[' . __('Error requesting data:', 'inline-gdocs-viewer') . ' ' . $resp->get_error_message() . ']');
    public function maybeFetchGvizDataSource () {
        if (
            !isset($_GET[$this->prefix . 'get_datasource_nonce'])
            ||
            !$this->isValidNonce($_GET[$this->prefix . 'get_datasource_nonce'], $this->prefix . 'get_datasource_nonce')
        ) { return; }
        require_once dirname(__FILE__) . '/lib/vistable.php';
        $url = rawurldecode($_GET['url']);
        $http_response = $this->fetchData($url, false);

        $p = parse_url($url);
        $qs = array();
        parse_str($p['query'], $qs);
        $tqx  = (isset($qs['tqx']))  ? $qs['tqx'] : '';
        $tq   = (isset($qs['tq']))   ? $qs['tq']  : '';
        $tqrt = (isset($qs['tqrt'])) ? $qs['tqrt']: '';
        $tz   = (isset($qs['tz']))   ? $qs['tz']  : 'PDT'; // TODO: will get_option('timezone_string') work?
        $vt = new csv_vistable($tqx, $tq, $tqrt, $tz, get_locale(), array());
        $vt->setup_table($http_response['body']);
        print $vt->execute();
        exit;
    }

    private function makeNonceUrl ($url) {
        $options = get_option($this->prefix . 'settings');
        $options[$this->prefix . 'get_datasource_nonce'] = wp_create_nonce($this->prefix . 'get_datasource_nonce');
        update_option($this->prefix . 'settings', $options);
        $p = parse_url($url);
        return $p['scheme']
            . '://' . $p['host'] . $p['path']
            . '?' . $p['query'] . '&'
            . $this->prefix . 'get_datasource_nonce=' . $options[$this->prefix . 'get_datasource_nonce'];
    }

    private function isValidNonce ($nonce, $nonce_name) {
        $options = get_option($this->prefix . 'settings');
        $is_valid = ($nonce === $options[$nonce_name]) ? true : false;
        unset($options[$this->prefix . 'get_datasource_nonce']);
        update_option($this->prefix . 'settings', $options);
        return $is_valid;
    }

                $url = $this->makeNonceUrl($this->getGVizDataSourceUrl($x['key'], $x['query'], $fmt));