CVE-2014-6300: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross-site scripting (XSS) vulnerability in the micro history implementation in phpMyAdmin 4.0.x before 4.0.10.3, 4.1.x before 4.1.14.4, and 4.2.x before 4.2.8.1 allows remote attackers to inject arbitrary web script or HTML, and consequently conduct a cross-site request forgery (CSRF) attack to create a root account, via a crafted URL, related to js/ajax.js.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Published: 2014-11-08T11:55Z
Repository: https://github.com/phpmyadmin/phpmyadmin
Language: PHP
Files changed: ChangeLog, js/ajax.js, js/whitelist.php, libraries/Header.class.php, libraries/Scripts.class.php
Commit date: 2014-09-13 10:30:47 -0400
Commit message: bug #4530 [security] DOM based XSS that results to a CSRF that creates a ROOT account in certain conditions

Signed-off-by: Marc Delisle <marc@infomarc.info>

Diff: diff --git a/ChangeLog b/ChangeLog
index 208a836236e7..08c97b8ac92e 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,8 +1,12 @@
 phpMyAdmin - ChangeLog
 ======================
 
+4.2.8.1 (not yet released)
+- bug #4530 [security] DOM based XSS that results to a CSRF that creates a
+            ROOT account in certain conditions
+
 4.2.8.0 (2014-08-31)
 - bug #4516 Odd export behavior
 - bug #4519 Uncaught TypeError: Cannot read property 'success' of null
 - bug #4520 sql.js: cannot read property
 - bug #4521 Initially allowed chart types do not match selected data
diff --git a/js/ajax.js b/js/ajax.js
index 08b3221af4fb..8de190e117c1 100644
--- a/js/ajax.js
+++ b/js/ajax.js
@@ -781,13 +781,20 @@ AJAX.setUrlHash = (function (jQuery, window) {
      * Start initialisation
      */
     if (window.location.hash.substring(0, 8) == '#PMAURL-') {
         // We have a valid hash, let's redirect the user
         // to the page that it's pointing to
-        window.location = window.location.hash.substring(
-            window.location.hash.indexOf(':') + 1
-        );
+        var colon_position = window.location.hash.indexOf(':');
+        var questionmark_position = window.location.hash.indexOf('?');
+        if (colon_position != -1 && questionmark_position != -1 && colon_position < questionmark_position) {
+            var hash_url = window.location.hash.substring(colon_position + 1, questionmark_position);
+            if (PMA_gotoWhitelist.indexOf(hash_url) != -1) {
+                window.location = window.location.hash.substring(
+                    colon_position + 1
+                );
+            }
+        }
     } else {
         // We don't have a valid hash, so we'll set it up
         // when the page finishes loading
         jQuery(function () {
             /* Check if we should set URL */
diff --git a/js/whitelist.php b/js/whitelist.php
new file mode 100644
index 000000000000..0f64f5ce32f2
--- /dev/null
+++ b/js/whitelist.php
@@ -0,0 +1,31 @@
+<?php
+/* vim: set expandtab sw=4 ts=4 sts=4: */
+/**
+ * Exporting of $goto_whitelist from PHP to Javascript
+ *
+ * @package PhpMyAdmin
+ */
+
+chdir('..');
+
+// Send correct type:
+header('Content-Type: text/javascript; charset=UTF-8');
+
+// Cache output in client - the nocache query parameter makes sure that this
+// file is reloaded when config changes
+header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 3600) . ' GMT');
+
+// Avoid loading the full common.inc.php because this would add many
+// non-js-compatible stuff like DOCTYPE
+define('PMA_MINIMUM_COMMON', true);
+require_once './libraries/common.inc.php';
+// Close session early as we won't write anything there
+session_write_close();
+
+echo "var PMA_gotoWhitelist = new Array();\n";
+$i = -1;
+foreach ($GLOBALS['goto_whitelist'] as $one_whitelist) {
+    $i++;
+    echo 'PMA_gotoWhitelist[' . $i . ']="' . $one_whitelist . '";' . "\n";
+}
+?>
diff --git a/libraries/Header.class.php b/libraries/Header.class.php
index bc1e4a6f1329..c54f8a03d9f8 100644
--- a/libraries/Header.class.php
+++ b/libraries/Header.class.php
@@ -142,11 +142,19 @@ public function __construct()
      *
      * @return void
      */
     private function _addDefaultScripts()
     {
+        // Localised strings
+        $params = array('lang' => $GLOBALS['lang']);
+        if (isset($GLOBALS['db'])) {
+            $params['db'] = $GLOBALS['db'];
+        }
         $this->_scripts->addFile('jquery/jquery-1.8.3.min.js');
+        $this->_scripts->addFile(
+            'whitelist.php' . PMA_URL_getCommon($params), false, true
+        );
         $this->_scripts->addFile('ajax.js');
         $this->_scripts->addFile('keyhandler.js');
         $this->_scripts->addFile('jquery/jquery-ui-1.9.2.custom.min.js');
         $this->_scripts->addFile('jquery/jquery.sprintf.js');
         $this->_scripts->addFile('jquery/jquery.cookie.js');
@@ -169,15 +177,10 @@ private function _addDefaultScripts()
         }
 
         // Here would not be a good place to add CodeMirror because
         // the user preferences have not been merged at this point
 
-        // Localised strings
-        $params = array('lang' => $GLOBALS['lang']);
-        if (isset($GLOBALS['db'])) {
-            $params['db'] = $GLOBALS['db'];
-        }
         $this->_scripts->addFile('messages.php' . PMA_URL_getCommon($params));
         // Append the theme id to this url to invalidate
         // the cache on a theme change. Though this might be
         // unavailable for fatal errors.
         if (isset($_SESSION['PMA_Theme'])) {
diff --git a/libraries/Scripts.class.php b/libraries/Scripts.class.php
index 7df3858796ee..7c5e1d1d509a 100644
--- a/libraries/Scripts.class.php
+++ b/libraries/Scripts.class.php
@@ -48,16 +48,22 @@ class PMA_Scripts
      *
      * @return string HTML code for javascript inclusion.
      */
     private function _includeFiles($files)
     {
+        $first_dynamic_scripts = "";
         $dynamic_scripts = "";
         $scripts = array();
         foreach ($files as $value) {
             if (strpos($value['filename'], "?") !== false) {
-                $dynamic_scripts .= "<script type='text/javascript' src='js/"
-                    . $value['filename'] . "'></script>";
+                if ($value['before_statics'] === true) {
+                    $first_dynamic_scripts .= "<script type='text/javascript' src='js/"
+                        . $value['filename'] . "'></script>";
+                } else {
+                    $dynamic_scripts .= "<script type='text/javascript' src='js/"
+                        . $value['filename'] . "'></script>";
+                }
                 continue;
             }
             $include = true;
             if ($value['conditional_ie'] !== false
                 && PMA_USR_BROWSER_AGENT === 'IE'
@@ -81,11 +87,11 @@ private function _includeFiles($files)
 
         $static_scripts = sprintf(
             '<script type="text/javascript" src="%s"></script>',
             htmlspecialchars($url)
         );
-        return $static_scripts . $dynamic_scripts;
+        return $first_dynamic_scripts . $static_scripts . $dynamic_scripts;
     }
 
     /**
      * Generates new PMA_Scripts objects
      *
@@ -103,25 +109,28 @@ public function __construct()
      * Adds a new file to the list of scripts
      *
      * @param string $filename       The name of the file to include
      * @param bool   $conditional_ie Whether to wrap the script tag in
      *                               conditional comments for IE
+     * @param bool   $before_statics Whether this dynamic script should be
+     *                               included before the static ones
      *
      * @return void
      */
-    public function addFile($filename, $conditional_ie = false)
+    public function addFile($filename, $conditional_ie = false, $before_statics = false)
     {
         $hash = md5($filename);
         if (!empty($this->_files[$hash])) {
             return;
         }
 
         $has_onload = $this->_eventBlacklist($filename);
         $this->_files[$hash] = array(
             'has_onload' => $has_onload,
             'filename' => $filename,
-            'conditional_ie' => $conditional_ie
+            'conditional_ie' => $conditional_ie,
+            'before_statics' => $before_statics
         );
     }
 
     /**
      * Determines whether to fire up an onload event for a file

Vulnerable code:         window.location = window.location.hash.substring(
            window.location.hash.indexOf(':') + 1
        );
        // Localised strings
        $params = array('lang' => $GLOBALS['lang']);
        if (isset($GLOBALS['db'])) {
            $params['db'] = $GLOBALS['db'];
        }
                $dynamic_scripts .= "<script type='text/javascript' src='js/"
                    . $value['filename'] . "'></script>";
        return $static_scripts . $dynamic_scripts;
    public function addFile($filename, $conditional_ie = false)
            'conditional_ie' => $conditional_ie
Fixed code: 4.2.8.1 (not yet released)
- bug #4530 [security] DOM based XSS that results to a CSRF that creates a
            ROOT account in certain conditions

        var colon_position = window.location.hash.indexOf(':');
        var questionmark_position = window.location.hash.indexOf('?');
        if (colon_position != -1 && questionmark_position != -1 && colon_position < questionmark_position) {
            var hash_url = window.location.hash.substring(colon_position + 1, questionmark_position);
            if (PMA_gotoWhitelist.indexOf(hash_url) != -1) {
                window.location = window.location.hash.substring(
                    colon_position + 1
                );
            }
        }
<?php
/* vim: set expandtab sw=4 ts=4 sts=4: */
/**
 * Exporting of $goto_whitelist from PHP to Javascript
 *
 * @package PhpMyAdmin
 */

chdir('..');

// Send correct type:
header('Content-Type: text/javascript; charset=UTF-8');

// Cache output in client - the nocache query parameter makes sure that this
// file is reloaded when config changes
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 3600) . ' GMT');

// Avoid loading the full common.inc.php because this would add many
// non-js-compatible stuff like DOCTYPE
define('PMA_MINIMUM_COMMON', true);
require_once './libraries/common.inc.php';
// Close session early as we won't write anything there
session_write_close();

echo "var PMA_gotoWhitelist = new Array();\n";
$i = -1;
foreach ($GLOBALS['goto_whitelist'] as $one_whitelist) {
    $i++;
    echo 'PMA_gotoWhitelist[' . $i . ']="' . $one_whitelist . '";' . "\n";
}
?>
        // Localised strings
        $params = array('lang' => $GLOBALS['lang']);
        if (isset($GLOBALS['db'])) {
            $params['db'] = $GLOBALS['db'];
        }
        $this->_scripts->addFile(
            'whitelist.php' . PMA_URL_getCommon($params), false, true
        );
        $first_dynamic_scripts = "";
                if ($value['before_statics'] === true) {
                    $first_dynamic_scripts .= "<script type='text/javascript' src='js/"
                        . $value['filename'] . "'></script>";
                } else {
                    $dynamic_scripts .= "<script type='text/javascript' src='js/"
                        . $value['filename'] . "'></script>";
                }
        return $first_dynamic_scripts . $static_scripts . $dynamic_scripts;
     * @param bool   $before_statics Whether this dynamic script should be
     *                               included before the static ones
    public function addFile($filename, $conditional_ie = false, $before_statics = false)
            'conditional_ie' => $conditional_ie,
            'before_statics' => $before_statics