From 9dbe520e905344597554b06931bd966ce195b9dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 18 Aug 2016 09:08:06 +0200 Subject: [PATCH 01/23] Strip null bytes from MySQL username MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In old PHP versions this could lead to allow/deny rules bypass. Signed-off-by: Michal Čihař --- libraries/core.lib.php | 18 ++++++++++++++++++ .../plugins/auth/AuthenticationCookie.php | 2 +- libraries/plugins/auth/AuthenticationHttp.php | 3 +++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/libraries/core.lib.php b/libraries/core.lib.php index 87aefa2210..e7cef9edb5 100644 --- a/libraries/core.lib.php +++ b/libraries/core.lib.php @@ -1029,6 +1029,24 @@ function PMA_sanitizeMySQLHost($name) return $name; } +/** + * Sanitizes MySQL username + * + * * strips part behind null byte + * + * @param string $name User given username + * + * @return string + */ +function PMA_sanitizeMySQLUser($name) +{ + $position = strpos($name, chr(0)); + if ($position !== false) { + return substr($name, 0, $position); + } + return $name; +} + /** * Safe unserializer wrapper * diff --git a/libraries/plugins/auth/AuthenticationCookie.php b/libraries/plugins/auth/AuthenticationCookie.php index 12e6c91bec..ab2baa039a 100644 --- a/libraries/plugins/auth/AuthenticationCookie.php +++ b/libraries/plugins/auth/AuthenticationCookie.php @@ -301,7 +301,7 @@ class AuthenticationCookie extends AuthenticationPlugin } // The user just logged in - $GLOBALS['PHP_AUTH_USER'] = $_REQUEST['pma_username']; + $GLOBALS['PHP_AUTH_USER'] = PMA_sanitizeMySQLUser($_REQUEST['pma_username']); $GLOBALS['PHP_AUTH_PW'] = empty($_REQUEST['pma_password']) ? '' : $_REQUEST['pma_password']; diff --git a/libraries/plugins/auth/AuthenticationHttp.php b/libraries/plugins/auth/AuthenticationHttp.php index 62dcc01690..0bad9ed657 100644 --- a/libraries/plugins/auth/AuthenticationHttp.php +++ b/libraries/plugins/auth/AuthenticationHttp.php @@ -167,6 +167,9 @@ class AuthenticationHttp extends AuthenticationPlugin unset($usr_pass); } + // sanitize username + $PHP_AUTH_USER = PMA_sanitizeMySQLUser($PHP_AUTH_USER); + // User logged out -> ensure the new username is not the same $old_usr = isset($_REQUEST['old_usr']) ? $_REQUEST['old_usr'] : ''; if (!empty($old_usr) From 2de86f02af17f49e22d6474542770d72dd4723b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 18 Aug 2016 09:15:34 +0200 Subject: [PATCH 02/23] Use hash_equals for comparing username in allow/deny rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comparison should happen in constant time to avoid possible leak of usernames in rules. Signed-off-by: Michal Čihař --- libraries/ip_allow_deny.lib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/ip_allow_deny.lib.php b/libraries/ip_allow_deny.lib.php index bd7c84757b..47092a969a 100644 --- a/libraries/ip_allow_deny.lib.php +++ b/libraries/ip_allow_deny.lib.php @@ -321,7 +321,7 @@ function PMA_allowDeny($type) // check for username if (($rule_data[1] != '%') //wildcarded first - && ($rule_data[1] != $username) + && (! hash_equals($rule_data[1], $username)) ) { continue; } From c83b2b0872c035d2625f631a341cb2009ceb9d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 18 Aug 2016 09:39:22 +0200 Subject: [PATCH 03/23] Use hash_equals for checking username MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes the comparison happen in constant time and makes it impossible to use it to guess stored usernames. Signed-off-by: Michal Čihař --- libraries/plugins/auth/AuthenticationCookie.php | 4 ++-- libraries/plugins/auth/AuthenticationHttp.php | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/plugins/auth/AuthenticationCookie.php b/libraries/plugins/auth/AuthenticationCookie.php index ab2baa039a..f9a5af8435 100644 --- a/libraries/plugins/auth/AuthenticationCookie.php +++ b/libraries/plugins/auth/AuthenticationCookie.php @@ -408,14 +408,14 @@ class AuthenticationCookie extends AuthenticationPlugin // Ensures valid authentication mode, 'only_db', bookmark database and // table names and relation table name are used - if ($cfg['Server']['user'] != $GLOBALS['PHP_AUTH_USER']) { + if (! hash_equals($cfg['Server']['user'], $GLOBALS['PHP_AUTH_USER'])) { foreach ($cfg['Servers'] as $idx => $current) { if ($current['host'] == $cfg['Server']['host'] && $current['port'] == $cfg['Server']['port'] && $current['socket'] == $cfg['Server']['socket'] && $current['ssl'] == $cfg['Server']['ssl'] && $current['connect_type'] == $cfg['Server']['connect_type'] - && $current['user'] == $GLOBALS['PHP_AUTH_USER'] + && hash_equals($current['user'], $GLOBALS['PHP_AUTH_USER']) ) { $GLOBALS['server'] = $idx; $cfg['Server'] = $current; diff --git a/libraries/plugins/auth/AuthenticationHttp.php b/libraries/plugins/auth/AuthenticationHttp.php index 0bad9ed657..62223f88a0 100644 --- a/libraries/plugins/auth/AuthenticationHttp.php +++ b/libraries/plugins/auth/AuthenticationHttp.php @@ -172,8 +172,8 @@ class AuthenticationHttp extends AuthenticationPlugin // User logged out -> ensure the new username is not the same $old_usr = isset($_REQUEST['old_usr']) ? $_REQUEST['old_usr'] : ''; - if (!empty($old_usr) - && (isset($PHP_AUTH_USER) && $old_usr == $PHP_AUTH_USER) + if (! empty($old_usr) + && (isset($PHP_AUTH_USER) && hash_equals($old_usr, $PHP_AUTH_USER)) ) { $PHP_AUTH_USER = ''; // -> delete user's choices that were stored in session @@ -207,12 +207,12 @@ class AuthenticationHttp extends AuthenticationPlugin // Ensures valid authentication mode, 'only_db', bookmark database and // table names and relation table name are used - if ($cfg['Server']['user'] != $PHP_AUTH_USER) { + if (! hash_equals($cfg['Server']['user'], $PHP_AUTH_USER)) { $servers_cnt = count($cfg['Servers']); for ($i = 1; $i <= $servers_cnt; $i++) { if (isset($cfg['Servers'][$i]) && ($cfg['Servers'][$i]['host'] == $cfg['Server']['host'] - && $cfg['Servers'][$i]['user'] == $PHP_AUTH_USER) + && hash_equals($cfg['Servers'][$i]['user'], $PHP_AUTH_USER)) ) { $server = $i; $cfg['Server'] = $cfg['Servers'][$i]; From 61976131350eca8cbba74270a44ea7baa708259f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 18 Aug 2016 09:48:51 +0200 Subject: [PATCH 04/23] Silent errors when getting remote file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - both curl and fopen wrappers can emmit errors in cases where remote site is not accessible - do not pass false value to json_decode Signed-off-by: Michal Čihař --- libraries/VersionInformation.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libraries/VersionInformation.php b/libraries/VersionInformation.php index a49a1b97b5..26e46d0741 100644 --- a/libraries/VersionInformation.php +++ b/libraries/VersionInformation.php @@ -72,7 +72,7 @@ class VersionInformation CURLOPT_TIMEOUT, $connection_timeout ); - $response = curl_exec($curl_handle); + $response = @curl_exec($curl_handle); } else if (ini_get('allow_url_fopen')) { $context = array( 'http' => array( @@ -81,12 +81,16 @@ class VersionInformation ) ); $context = Util::handleContext($context); - $response = file_get_contents( + $response = @file_get_contents( $file, false, stream_context_create($context) ); } + // Check possible failure of getting data + if ($response === false) { + $response = '{}'; + } } /* Parse response */ From cf83d6afbe4383e7668c1207be6d5bee0646be04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 18 Aug 2016 09:59:15 +0200 Subject: [PATCH 05/23] Remove debugging code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michal Čihař --- libraries/export.lib.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/libraries/export.lib.php b/libraries/export.lib.php index 20567d9698..e73c7ef32e 100644 --- a/libraries/export.lib.php +++ b/libraries/export.lib.php @@ -18,12 +18,10 @@ use PMA\libraries\ZipFile; */ function PMA_shutdownDuringExport() { - $a = error_get_last(); - if ($a != null && mb_strpos($a['message'], "execution time")) { - //write in partially downloaded file for future reference of user - print_r($a); + $error = error_get_last(); + if ($error != null && mb_strpos($error['message'], "execution time")) { //set session variable to check if there was error while exporting - $_SESSION['pma_export_error'] = $a['message']; + $_SESSION['pma_export_error'] = $error['message']; } } From a0ca9394a214afd15a4b883f8953b7b32f3caf04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 18 Aug 2016 10:03:01 +0200 Subject: [PATCH 06/23] Strip path even if openbasedir restrictions apply MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This really should not be the case here as what we get here is code executed by PHP, so it should have already passed openbasedir restrictions. Signed-off-by: Michal Čihař --- libraries/Error.php | 2 +- test/classes/ErrorTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/Error.php b/libraries/Error.php index 6548877c60..239e186012 100644 --- a/libraries/Error.php +++ b/libraries/Error.php @@ -460,7 +460,7 @@ class Error extends Message /* Probably affected by open_basedir */ if ($dest === FALSE) { - return $path; + return basename($path); } $Ahere = explode( diff --git a/test/classes/ErrorTest.php b/test/classes/ErrorTest.php index ae2aca6b97..fa5c6e9dce 100644 --- a/test/classes/ErrorTest.php +++ b/test/classes/ErrorTest.php @@ -102,7 +102,7 @@ class ErrorTest extends PMATestCase return array( array('./ChangeLog', './ChangeLog'), array(__FILE__, './test/classes/ErrorTest.php'), - array('./NONEXISTING', './NONEXISTING'), + array('./NONEXISTING', 'NONEXISTING'), ); } From 6e3282e15856192d484f2c56a7ae83796bf2e716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 18 Aug 2016 11:05:56 +0200 Subject: [PATCH 07/23] Store copy of hash instead of working on live object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This avoids possible race conditions when doing the checks. Signed-off-by: Michal Čihař --- js/microhistory.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/js/microhistory.js b/js/microhistory.js index 60bb207147..4263cd0eef 100644 --- a/js/microhistory.js +++ b/js/microhistory.js @@ -280,15 +280,16 @@ PMA_SetUrlHash = (function (jQuery, window) { /** * Start initialisation */ - if (window.location.hash.substring(0, 8) == '#PMAURL-') { + var urlhash = window.location.hash; + if (urlhash.substring(0, 8) == '#PMAURL-') { // We have a valid hash, let's redirect the user // to the page that it's pointing to - var colon_position = window.location.hash.indexOf(':'); - var questionmark_position = window.location.hash.indexOf('?'); + var colon_position = urlhash.indexOf(':'); + var questionmark_position = urlhash.indexOf('?'); if (colon_position != -1 && questionmark_position != -1 && colon_position < questionmark_position) { - var hash_url = window.location.hash.substring(colon_position + 1, questionmark_position); + var hash_url = urlhash.substring(colon_position + 1, questionmark_position); if (PMA_gotoWhitelist.indexOf(hash_url) != -1) { - window.location = window.location.hash.substring( + window.location = urlhash.substring( colon_position + 1 ); } @@ -328,4 +329,4 @@ PMA_SetUrlHash = (function (jQuery, window) { * Publicly exposes a reference to the otherwise private setUrlHash function */ return setUrlHash; -})(jQuery, window); \ No newline at end of file +})(jQuery, window); From b63bd9977c436f6ad564d429e663bb580bb4e730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 18 Aug 2016 11:18:21 +0200 Subject: [PATCH 08/23] Stricter validation of NavigationLogoLink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It now has to be URL including scheme. Otherwise it's not really possible to validate it for being just http/https. Signed-off-by: Michal Čihař --- libraries/navigation/NavigationHeader.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/libraries/navigation/NavigationHeader.php b/libraries/navigation/NavigationHeader.php index 6b1b5e8c13..bd8d59bef2 100644 --- a/libraries/navigation/NavigationHeader.php +++ b/libraries/navigation/NavigationHeader.php @@ -106,11 +106,9 @@ class NavigationHeader $logoLink = trim( htmlspecialchars($GLOBALS['cfg']['NavigationLogoLink']) ); - // prevent XSS, see PMASA-2013-9 - // if link has protocol, allow only http and https - if (preg_match('/^[a-z]+:/i', $logoLink) - && !preg_match('/^https?:/i', $logoLink) - ) { + $parsed = parse_url($logoLink); + /* Allow only links with http/https */ + if (! isset($parsed['scheme']) || ! in_array(strtolower($parsed['scheme']), array('http', 'https'))) { $logoLink = 'index.php'; } switch ($GLOBALS['cfg']['NavigationLogoLinkWindow']) { From 3ef6201bd6d43eed7f360bb2ead22ef7c484381e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 18 Aug 2016 11:25:21 +0200 Subject: [PATCH 09/23] Fix hash validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - use copy of hash to avoid race condition - stricter regex to match whole string Signed-off-by: Michal Čihař --- js/config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/config.js b/js/config.js index 6630dd6c7c..3a07504061 100644 --- a/js/config.js +++ b/js/config.js @@ -624,9 +624,9 @@ AJAX.registerOnload('config.js', function () { var tab_check_fnc = function () { if (location.hash != prev_hash) { prev_hash = location.hash; - if (location.hash.match(/^#tab_[a-zA-Z0-9_]+/)) { + if (prev_hash.match(/^#tab_[a-zA-Z0-9_]+$/)) { // session ID is sometimes appended here - var hash = location.hash.substr(5).split('&')[0]; + var hash = prev_hash.substr(5).split('&')[0]; if ($('#' + hash).length) { setTab(hash); } From 45e33d63a252011b02b55b74432c4a90484eaa5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 18 Aug 2016 16:55:48 +0200 Subject: [PATCH 10/23] Limit maximal number of rows in QBE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User would be lost in them anyway by that count and it prevents DOS. Signed-off-by: Michal Čihař --- libraries/DbQbe.php | 5 ++++- libraries/SavedSearches.php | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/libraries/DbQbe.php b/libraries/DbQbe.php index d9d1f3ca21..08eb4cc520 100644 --- a/libraries/DbQbe.php +++ b/libraries/DbQbe.php @@ -1940,7 +1940,10 @@ class DbQbe // sets row count $rows = PMA_ifSetOr($_REQUEST['rows'], 0, 'numeric'); $criteriaRowAdd = PMA_ifSetOr($_REQUEST['criteriaRowAdd'], 0, 'numeric'); - $this->_criteria_row_count = max($rows + $criteriaRowAdd, 0); + $this->_criteria_row_count = min( + 100, + max($rows + $criteriaRowAdd, 0) + ); return $criteriaColumnCount; } diff --git a/libraries/SavedSearches.php b/libraries/SavedSearches.php index ccbd9c5f95..6c2c0c29e1 100644 --- a/libraries/SavedSearches.php +++ b/libraries/SavedSearches.php @@ -160,6 +160,16 @@ class SavedSearches } } + /* Limit amount of rows */ + if (!isset($data['rows'])) { + $data['rows'] = 0; + } else { + $data['rows'] = min( + max(0, intval($data['rows'])), + 100 + ); + } + for ($i = 0; $i <= $data['rows']; $i++) { $data['Or' . $i] = $criterias['Or' . $i]; } From 283f5d10ce65e0656f98335bd1f2767fba3be6db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 2 Sep 2016 13:42:15 +0200 Subject: [PATCH 11/23] Fix possible DOS on too big skip value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - loop only as long as long we have data to skip - convert skip parameter to integer Signed-off-by: Michal Čihař --- import.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/import.php b/import.php index 5afdd869b4..fbb6f09dae 100644 --- a/import.php +++ b/import.php @@ -613,8 +613,8 @@ if ($GLOBALS['PMA_recoding_engine'] != PMA_CHARSET_NONE && isset($charset_of_fil // Something to skip? (because timeout has passed) if (! $error && isset($_POST['skip'])) { - $original_skip = $skip = $_POST['skip']; - while ($skip > 0) { + $original_skip = $skip = intval($_POST['skip']); + while ($skip > 0 && ! $finished) { PMA_importGetNextChunk($skip < $read_limit ? $skip : $read_limit); // Disable read progressivity, otherwise we eat all memory! $read_multiply = 1; From 7ddcbc0de6283f42fc0592901d88ee53b457bee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 2 Sep 2016 13:54:15 +0200 Subject: [PATCH 12/23] Validate (sub)partion count from request before use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michal Čihař --- libraries/tbl_partition_definition.inc.php | 40 ++++++++++++++-------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/libraries/tbl_partition_definition.inc.php b/libraries/tbl_partition_definition.inc.php index 3f7602bb05..0ec3dc6f0b 100644 --- a/libraries/tbl_partition_definition.inc.php +++ b/libraries/tbl_partition_definition.inc.php @@ -14,17 +14,31 @@ if (!isset($partitionDetails)) { // Extract some partitioning and subpartitioning parameters from the request $partitionParams = array( - 'partition_by', 'partition_expr', 'partition_count', - 'subpartition_by', 'subpartition_expr', 'subpartition_count' + 'partition_by', 'partition_expr', + 'subpartition_by', 'subpartition_expr', ); foreach ($partitionParams as $partitionParam) { $partitionDetails[$partitionParam] = isset($_REQUEST[$partitionParam]) ? $_REQUEST[$partitionParam] : ''; } + if (PMA_isValid($_REQUEST['partition_count'], 'numeric')) { + // MySQL's limit is 8192, so do not allow more + $partition_count = min(intval($_REQUEST['partition_count']), 8192); + } else { + $partition_count = 0; + } + $partitionDetails['partition_count'] = $partition_count; + if (PMA_isValid($_REQUEST['subpartition_count'], 'numeric')) { + // MySQL's limit is 8192, so do not allow more + $subpartition_count = min(intval($_REQUEST['subpartition_count']), 8192); + } else { + $subpartition_count = 0; + } + $partitionDetails['subpartition_count'] = $subpartition_count; + // Only LIST and RANGE type parameters allow subpartitioning - $partitionDetails['can_have_subpartitions'] = isset($_REQUEST['partition_count']) - && $_REQUEST['partition_count'] > 1 + $partitionDetails['can_have_subpartitions'] = $partition_count > 1 && isset($_REQUEST['partition_by']) && ($_REQUEST['partition_by'] == 'RANGE' || $_REQUEST['partition_by'] == 'RANGE COLUMNS' @@ -38,18 +52,17 @@ if (!isset($partitionDetails)) { || $_REQUEST['partition_by'] == 'LIST' || $_REQUEST['partition_by'] == 'LIST COLUMNS'); - if (PMA_isValid($_REQUEST['partition_count'], 'numeric') - && $_REQUEST['partition_count'] > 1 - ) { // Has partitions + // Has partitions + if ($partition_count > 1) { $partitions = isset($_REQUEST['partitions']) ? $_REQUEST['partitions'] : array(); // Remove details of the additional partitions // when number of partitions have been reduced - array_splice($partitions, $_REQUEST['partition_count']); + array_splice($partitions, $partition_count); - for ($i = 0; $i < $_REQUEST['partition_count']; $i++) { + for ($i = 0; $i < $partition_count; $i++) { if (! isset($partitions[$i])) { // Newly added partition $partitions[$i] = array( 'name' => 'p' . $i, @@ -85,11 +98,10 @@ if (!isset($partitionDetails)) { $partition['node_group'] = ''; } - if (PMA_isValid($_REQUEST['subpartition_count'], 'numeric') - && $_REQUEST['subpartition_count'] > 1 + if ($subpartition_count > 1 && $partitionDetails['can_have_subpartitions'] == true ) { // Has subpartitions - $partition['subpartition_count'] = $_REQUEST['subpartition_count']; + $partition['subpartition_count'] = $subpartition_count; if (! isset($partition['subpartitions'])) { $partition['subpartitions'] = array(); @@ -98,9 +110,9 @@ if (!isset($partitionDetails)) { // Remove details of the additional subpartitions // when number of subpartitions have been reduced - array_splice($subpartitions, $_REQUEST['subpartition_count']); + array_splice($subpartitions, $subpartition_count); - for ($j = 0; $j < $_REQUEST['subpartition_count']; $j++) { + for ($j = 0; $j < $subpartition_count; $j++) { if (! isset($subpartitions[$j])) { // Newly added subpartition $subpartitions[$j] = array( 'name' => $partition['name'] . '_s' . $j, From 733a5d582193d32c5435bd6324316c9da1b01678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 2 Sep 2016 14:17:52 +0200 Subject: [PATCH 13/23] Avoid rendering BB code when showing PHP/MySQL errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michal Čihař --- libraries/ErrorHandler.php | 9 +++++--- libraries/Message.php | 25 +++++++++++++++++++++- libraries/plugins/AuthenticationPlugin.php | 2 +- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/libraries/ErrorHandler.php b/libraries/ErrorHandler.php index 8bd408d5a1..31beca5bdb 100644 --- a/libraries/ErrorHandler.php +++ b/libraries/ErrorHandler.php @@ -175,16 +175,19 @@ class ErrorHandler $this->errors[$error->getHash()] = $error; switch ($error->getNumber()) { - case E_USER_NOTICE: - case E_USER_WARNING: case E_STRICT: case E_DEPRECATED: case E_NOTICE: case E_WARNING: case E_CORE_WARNING: case E_COMPILE_WARNING: - case E_USER_ERROR: case E_RECOVERABLE_ERROR: + /* Avoid rendering BB code in PHP errors */ + $error->setBBCode(false); + break; + case E_USER_NOTICE: + case E_USER_WARNING: + case E_USER_ERROR: // just collect the error // display is called from outside break; diff --git a/libraries/Message.php b/libraries/Message.php index 1b500aff39..fefd50ff44 100644 --- a/libraries/Message.php +++ b/libraries/Message.php @@ -112,6 +112,14 @@ class Message */ protected $isDisplayed = false; + /** + * Whether to use BB code when displaying. + * + * @access protected + * @var boolean + */ + protected $useBBCode = true; + /** * Unique id * @@ -236,6 +244,7 @@ class Message { $r = new Message('', $type); $r->setMessage($message); + $r->setBBCode(false); return $r; } @@ -392,6 +401,18 @@ class Message return $this->getNumber() === Message::ERROR; } + /** + * Set whether we should use BB Code when rendering. + * + * @param boolean $useBBCode Use BB Code? + * + * @return void + */ + public function setBBCode($useBBCode) + { + $this->useBBCode = $useBBCode; + } + /** * set raw message (overrides string) * @@ -647,7 +668,9 @@ class Message $message = Message::format($message, $this->getParams()); } - $message = Message::decodeBB($message); + if ($this->useBBCode) { + $message = Message::decodeBB($message); + } foreach ($this->getAddedMessages() as $add_message) { $message .= $add_message; diff --git a/libraries/plugins/AuthenticationPlugin.php b/libraries/plugins/AuthenticationPlugin.php index 33f83f4a63..3a435d22f0 100644 --- a/libraries/plugins/AuthenticationPlugin.php +++ b/libraries/plugins/AuthenticationPlugin.php @@ -114,7 +114,7 @@ abstract class AuthenticationPlugin } else { $dbi_error = $GLOBALS['dbi']->getError(); if (!empty($dbi_error)) { - return PMA_sanitize($dbi_error); + return htmlspecialchars($dbi_error); } elseif (isset($GLOBALS['errno'])) { return '#' . $GLOBALS['errno'] . ' ' . __('Cannot log in to the MySQL server'); From dac36c3cd889f87165ae776e3929fe9f946f67ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 2 Sep 2016 14:45:21 +0200 Subject: [PATCH 14/23] Stricter URL validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - do not use empty() as empty('0') is true - do not lowercase the strings, use them as they are - lowercase all domains in our codebase - do not allow to specify port Signed-off-by: Michal Čihař --- index.php | 2 +- libraries/core.lib.php | 14 +++++++++++--- test/libraries/core/PMA_isAllowedDomain_test.php | 3 +++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/index.php b/index.php index 5fe048ab43..2934cd1135 100644 --- a/index.php +++ b/index.php @@ -389,7 +389,7 @@ PMA_printListItem( PMA_printListItem( __('Official Homepage'), 'li_pma_homepage', - PMA_linkURL('https://www.phpMyAdmin.net/'), + PMA_linkURL('https://www.phpmyadmin.net/'), null, '_blank' ); diff --git a/libraries/core.lib.php b/libraries/core.lib.php index 373d31a328..e5997a5608 100644 --- a/libraries/core.lib.php +++ b/libraries/core.lib.php @@ -754,10 +754,17 @@ function PMA_linkURL($url) function PMA_isAllowedDomain($url) { $arr = parse_url($url); - // Avoid URLs without hostname or with credentials - if (empty($arr['host']) || ! empty($arr['user']) || ! empty($arr['pass'])) { + // We need host to be set + if (! isset($arr['host']) || strlen($arr['host']) == 0) { return false; } + // We do not want these to be present + $blocked = array('user', 'pass', 'port'); + foreach ($blocked as $part) { + if (isset($arr[$part]) && strlen($arr[$part]) != 0) { + return false; + } + } $domain = $arr["host"]; $domainWhiteList = array( /* Include current domain */ @@ -766,6 +773,7 @@ function PMA_isAllowedDomain($url) 'wiki.phpmyadmin.net', 'www.phpmyadmin.net', 'phpmyadmin.net', 'demo.phpmyadmin.net', 'docs.phpmyadmin.net', + 'demo.phpmyadmin.net', /* mysql.com domains */ 'dev.mysql.com','bugs.mysql.com', /* mariadb domains */ @@ -781,7 +789,7 @@ function PMA_isAllowedDomain($url) /* Following are doubtful ones. */ 'mysqldatabaseadministration.blogspot.com', ); - if (in_array(mb_strtolower($domain), $domainWhiteList)) { + if (in_array($domain, $domainWhiteList)) { return true; } diff --git a/test/libraries/core/PMA_isAllowedDomain_test.php b/test/libraries/core/PMA_isAllowedDomain_test.php index 586e6bcfb3..9f544c0d9c 100644 --- a/test/libraries/core/PMA_isAllowedDomain_test.php +++ b/test/libraries/core/PMA_isAllowedDomain_test.php @@ -43,6 +43,9 @@ class PMA_isAllowedDomain_test extends PHPUnit_Framework_TestCase array('https://www.phpmyadmin.net/', true), array('http://duckduckgo.com\\@github.com', false), array('https://github.com/', true), + array('https://github.com:123/', false), + array('https://user:pass@github.com:123/', false), + array('https://user:pass@github.com/', false), array('https://server.local/', true), array('./relative/', false), ); From 9f0c4c37f3067abcaa7a40b9ef907e343ee62de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 2 Sep 2016 15:04:46 +0200 Subject: [PATCH 15/23] Fix test failures with recent changes to Message class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michal Čihař --- test/classes/MessageTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/classes/MessageTest.php b/test/classes/MessageTest.php index b330430b5b..203178f09b 100644 --- a/test/classes/MessageTest.php +++ b/test/classes/MessageTest.php @@ -110,6 +110,7 @@ class MessageTest extends PMATestCase { $this->object = new PMA\libraries\Message('', PMA\libraries\Message::ERROR); $this->object->setMessage('test<&>'); + $this->object->setBBCode(false); $this->assertEquals($this->object, PMA\libraries\Message::rawError('test<&>')); } @@ -123,6 +124,7 @@ class MessageTest extends PMATestCase { $this->object = new PMA\libraries\Message('', PMA\libraries\Message::NOTICE); $this->object->setMessage('test<&>'); + $this->object->setBBCode(false); $this->assertEquals($this->object, PMA\libraries\Message::rawNotice('test<&>')); } @@ -136,6 +138,7 @@ class MessageTest extends PMATestCase { $this->object = new PMA\libraries\Message('', PMA\libraries\Message::SUCCESS); $this->object->setMessage('test<&>'); + $this->object->setBBCode(false); $this->assertEquals($this->object, PMA\libraries\Message::rawSuccess('test<&>')); } From 2f2e3565ba1e85072d2c1a1f0a06b84bebce8f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 2 Sep 2016 15:09:29 +0200 Subject: [PATCH 16/23] Use hash_equals when comparing IPv6 allow rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michal Čihař --- libraries/ip_allow_deny.lib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/ip_allow_deny.lib.php b/libraries/ip_allow_deny.lib.php index 47092a969a..cc74c16b7c 100644 --- a/libraries/ip_allow_deny.lib.php +++ b/libraries/ip_allow_deny.lib.php @@ -194,7 +194,7 @@ function PMA_ipv6MaskTest($test_range, $ip_to_test) if ($is_single) { $range_hex = bin2hex(inet_pton($test_range)); - $result = $ip_hex === $range_hex; + $result = hash_equals($ip_hex, $range_hex); return $result; } From fbad6b9b4d175aa82cd35972aa2dcd9ef3ec4346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 19 Sep 2016 11:43:08 +0200 Subject: [PATCH 17/23] Verify value of access_time to avoid unwanted session extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We need to ansure the access_time parameter is in valid range to avoid possibility of remotely extending session validity. Signed-off-by: Michal Čihař --- libraries/plugins/AuthenticationPlugin.php | 22 +++++++++++++++++++ .../plugins/auth/AuthenticationConfig.php | 8 +------ .../plugins/auth/AuthenticationCookie.php | 6 +---- libraries/plugins/auth/AuthenticationHttp.php | 4 +--- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/libraries/plugins/AuthenticationPlugin.php b/libraries/plugins/AuthenticationPlugin.php index 3a435d22f0..6063aa98e0 100644 --- a/libraries/plugins/AuthenticationPlugin.php +++ b/libraries/plugins/AuthenticationPlugin.php @@ -134,4 +134,26 @@ abstract class AuthenticationPlugin public function handlePasswordChange($password) { } + + /** + * Store session access time in session. + * + * Tries to workaround PHP 5 session garbage collection which + * looks at the session file's last modified time + * + * @return void + */ + public function setSessionAccessTime() + { + if (isset($_REQUEST['access_time'])) { + // Ensure access_time is in range <0, LoginCookieValidity + 1> + // to avoid excessive extension of validity. + // + // Negative values can cause session expiry extension + // Too big values can cause overflow and lead to same + $_SESSION['last_access_time'] = time() - min(max(0, intval($_REQUEST['access_time'])), $GLOBALS['cfg']['LoginCookieValidity'] + 1); + } else { + $_SESSION['last_access_time'] = time(); + } + } } diff --git a/libraries/plugins/auth/AuthenticationConfig.php b/libraries/plugins/auth/AuthenticationConfig.php index a98263b94f..77bf980d97 100644 --- a/libraries/plugins/auth/AuthenticationConfig.php +++ b/libraries/plugins/auth/AuthenticationConfig.php @@ -61,13 +61,7 @@ class AuthenticationConfig extends AuthenticationPlugin */ public function authSetUser() { - // try to workaround PHP 5 session garbage collection which - // looks at the session file's last modified time - if (isset($_REQUEST['access_time'])) { - $_SESSION['last_access_time'] = time() - $_REQUEST['access_time']; - } else { - $_SESSION['last_access_time'] = time(); - } + $this->setSessionAccessTime(); return true; } diff --git a/libraries/plugins/auth/AuthenticationCookie.php b/libraries/plugins/auth/AuthenticationCookie.php index f953c4c150..0d37f4db32 100644 --- a/libraries/plugins/auth/AuthenticationCookie.php +++ b/libraries/plugins/auth/AuthenticationCookie.php @@ -450,11 +450,7 @@ class AuthenticationCookie extends AuthenticationPlugin // Avoid showing the password in phpinfo()'s output unset($GLOBALS['PHP_AUTH_PW']); unset($_SERVER['PHP_AUTH_PW']); - if (isset($_REQUEST['access_time'])) { - $_SESSION['last_access_time'] = time() - $_REQUEST['access_time']; - } else { - $_SESSION['last_access_time'] = time(); - } + $this->setSessionAccessTime(); } /** diff --git a/libraries/plugins/auth/AuthenticationHttp.php b/libraries/plugins/auth/AuthenticationHttp.php index 62223f88a0..3d33186478 100644 --- a/libraries/plugins/auth/AuthenticationHttp.php +++ b/libraries/plugins/auth/AuthenticationHttp.php @@ -228,9 +228,7 @@ class AuthenticationHttp extends AuthenticationPlugin unset($GLOBALS['PHP_AUTH_PW']); unset($_SERVER['PHP_AUTH_PW']); - // try to workaround PHP 5 session garbage collection which - // looks at the session file's last modified time - $_SESSION['last_access_time'] = time(); + $this->setSessionAccessTime(); return true; } From f87358dff730e7a5ada13922108151a9eee107cf Mon Sep 17 00:00:00 2001 From: Deven Bansod Date: Sun, 2 Oct 2016 10:35:29 +0530 Subject: [PATCH 18/23] Don't assume the default arg_separator in URL Respect the value for arg_separator.input too. Signed-off-by: Deven Bansod --- prefs_manage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prefs_manage.php b/prefs_manage.php index 44964e70de..c58f47829d 100644 --- a/prefs_manage.php +++ b/prefs_manage.php @@ -173,7 +173,7 @@ if (isset($_POST['submit_export']) $result = PMA_saveUserprefs($cf->getConfigArray()); if ($result === true) { if ($return_url) { - $query = explode('&', parse_url($return_url, PHP_URL_QUERY)); + $query = PMA\libraries\Util::splitURLQuery($return_url); $return_url = parse_url($return_url, PHP_URL_PATH); foreach ($query as $q) { From 17b34be04f5cc2a5f83d73eba7cd41cbc3ebb7b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Tue, 4 Oct 2016 13:17:07 +0200 Subject: [PATCH 19/23] Correctly parse string length when checking serialized data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michal Čihař --- libraries/core.lib.php | 2 +- test/libraries/core/PMA_safeUnserialize_test.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/core.lib.php b/libraries/core.lib.php index e5997a5608..ef79c916d3 100644 --- a/libraries/core.lib.php +++ b/libraries/core.lib.php @@ -1088,7 +1088,7 @@ function PMA_safeUnserialize($data) case 's': /* string */ // parse sting length - $strlen = intval($data[$i + 2]); + $strlen = intval(substr($data, $i + 2)); // string start $i = strpos($data, ':', $i + 2); if ($i === false) { diff --git a/test/libraries/core/PMA_safeUnserialize_test.php b/test/libraries/core/PMA_safeUnserialize_test.php index f2710c0792..f589361b93 100644 --- a/test/libraries/core/PMA_safeUnserialize_test.php +++ b/test/libraries/core/PMA_safeUnserialize_test.php @@ -44,6 +44,7 @@ class PMA_safeUnserialize_test extends PHPUnit_Framework_TestCase array('b:0;', false), array('O:1:"a":1:{s:5:"value";s:3:"100";}', null), array('O:8:"stdClass":1:{s:5:"field";O:8:"stdClass":0:{}}', null), + array('a:2:{i:0;s:90:"1234567890;a345678901234567890123456789012345678901234567890123456789012345678901234567890";i:1;O:8:"stdClass":0:{}}', null), array(serialize(array(1, 2, 3)), array(1, 2, 3)), array(serialize('string""'), 'string""'), array(serialize(array('foo' => 'bar')), array('foo' => 'bar')), From 3b8fc504d2a3c1da692cfe703d43d6d81cdc5778 Mon Sep 17 00:00:00 2001 From: Deven Bansod Date: Fri, 7 Oct 2016 10:11:59 +0530 Subject: [PATCH 20/23] Manage new-lines and extra whitespaces properly Signed-off-by: Deven Bansod --- libraries/tracking.lib.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/tracking.lib.php b/libraries/tracking.lib.php index d435368cb2..c2b63068c8 100644 --- a/libraries/tracking.lib.php +++ b/libraries/tracking.lib.php @@ -1186,14 +1186,16 @@ function PMA_exportAsFileDownload($entries) { @ini_set('url_rewriter.tags', ''); + // Replace all multiple whitespaces by a single space + $table = htmlspecialchars(preg_replace('/\s+/', ' ', $_REQUEST['table'])); $dump = "# " . sprintf( - __('Tracking report for table `%s`'), htmlspecialchars($_REQUEST['table']) + __('Tracking report for table `%s`'), $table ) . "\n" . "# " . date('Y-m-d H:i:s') . "\n"; foreach ($entries as $entry) { $dump .= $entry['statement']; } - $filename = 'log_' . htmlspecialchars($_REQUEST['table']) . '.sql'; + $filename = 'log_' . $table . '.sql'; PMA\libraries\Response::getInstance()->disable(); PMA_downloadHeader( $filename, From d74714c45ec246d179aa4b19d81c30903cdff876 Mon Sep 17 00:00:00 2001 From: Deven Bansod Date: Tue, 11 Oct 2016 10:46:35 +0530 Subject: [PATCH 21/23] Manage new-lines and extra whitespaces properly Signed-off-by: Deven Bansod --- libraries/Tracker.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/Tracker.php b/libraries/Tracker.php index a4cb56c99f..91ff014b03 100644 --- a/libraries/Tracker.php +++ b/libraries/Tracker.php @@ -144,8 +144,9 @@ class Tracker static public function getLogComment() { $date = date('Y-m-d H:i:s'); + $user = preg_replace('/\s+/', ' ', $GLOBALS['cfg']['Server']['user']); - return "# log " . $date . " " . $GLOBALS['cfg']['Server']['user'] . "\n"; + return "# log " . $date . " " . $user . "\n"; } /** From 8119464150081bcc18641281d62991bdff5feff7 Mon Sep 17 00:00:00 2001 From: Deven Bansod Date: Tue, 8 Nov 2016 10:11:01 +0530 Subject: [PATCH 22/23] Handle multiple `:p` while sanitizing MySQL hosts Signed-off-by: Deven Bansod --- libraries/core.lib.php | 6 +++--- test/libraries/core/PMA_sanitizeMySQLHost_test.php | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/libraries/core.lib.php b/libraries/core.lib.php index ef79c916d3..cb7161d281 100644 --- a/libraries/core.lib.php +++ b/libraries/core.lib.php @@ -1022,7 +1022,7 @@ if (! function_exists('hash_hmac')) { /** * Sanitizes MySQL hostname * - * * strips p: prefix + * * strips p: prefix(es) * * @param string $name User given hostname * @@ -1030,8 +1030,8 @@ if (! function_exists('hash_hmac')) { */ function PMA_sanitizeMySQLHost($name) { - if (strtolower(substr($name, 0, 2)) == 'p:') { - return substr($name, 2); + while (strtolower(substr($name, 0, 2)) == 'p:') { + $name = substr($name, 2); } return $name; diff --git a/test/libraries/core/PMA_sanitizeMySQLHost_test.php b/test/libraries/core/PMA_sanitizeMySQLHost_test.php index 1b4404732a..d17ca530d0 100644 --- a/test/libraries/core/PMA_sanitizeMySQLHost_test.php +++ b/test/libraries/core/PMA_sanitizeMySQLHost_test.php @@ -40,6 +40,7 @@ class PMA_sanitizeMySQLHost_test extends PHPUnit_Framework_TestCase { return array( array('p:foo.bar', 'foo.bar'), + array('p:p:foo.bar', 'foo.bar'), array('bar.baz', 'bar.baz'), array('P:example.com', 'example.com'), ); From 5daee71272efd87a24fe58f90303e82a5b6caf6d Mon Sep 17 00:00:00 2001 From: Isaac Bennetch Date: Thu, 24 Nov 2016 11:02:33 -0500 Subject: [PATCH 23/23] 4.6.5 release and ChangeLog Signed-off-by: Isaac Bennetch --- ChangeLog | 16 +++++++++++++++- README | 2 +- doc/conf.py | 2 +- libraries/Config.php | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/ChangeLog b/ChangeLog index 6c7461e8f1..3fc3d238e3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,7 +1,7 @@ phpMyAdmin - ChangeLog ====================== -4.6.5 (not yet released) +4.6.5 (2016-11-24) - issue Remove potentionally license problematic sRGB profile - issue #12459 Display read only fields as read only when editing - issue #12384 Fix expanding of navigation pane when clicking on database @@ -86,6 +86,20 @@ phpMyAdmin - ChangeLog - issue #12257 Improved search page performance - issue #12322 Avoid selecting default function for foreign keys - issue #12453 Fixed escaping of SQL parts in some corner cases +- issue [security] Unsafe generation of $cfg['blowfish_secret'], see PMASA-2016-58 +- issue [security] phpMyAdmin's phpinfo functionality is removed, see PMASA-2016-59 +- issue [security] AllowRoot and allow/deny rule bypass with specially-crafted username, see PMASA-2016-60 +- issue [security] Username matching weaknesses with allow/deny rules, see PMASA-2016-61 +- issue [security] Possible to bypass logout timeout, see PMASA-2016-62 +- issue [security] Full path disclosure (FPD) weaknesses, see PMASA-2016-63 +- issue [security] Multiple XSS weaknesses, see PMASA-2016-64 +- issue [security] Multiple denial-of-service (DOS) vulnerabilities, see PMASA-2016-65 +- issue [security] Possible to bypass white-list protection for URL redirection, see PMASA-2016-66 +- issue [security] BBCode injection to login page, see PMASA-2016-67 +- issue [security] Denial-of-service (DOS) vulnerability in table partitioning, see PMASA-2016-68 +- issue [security] Multiple SQL injection vulnerabilities, see PMASA-2016-69 +- issue [security] Incorrect serialized string parsing, see PMASA-2016-70 +- issue [security] CSRF token not stripped from the URL, see PMASA-2016-71 4.6.4 (2016-08-16) - issue [security] Weaknesses with cookie encryption, see PMASA-2016-29 diff --git a/README b/README index 6e7b903c38..48afce6480 100644 --- a/README +++ b/README @@ -1,7 +1,7 @@ phpMyAdmin - Readme =================== -Version 4.6.5-dev +Version 4.6.5 A web interface for MySQL and MariaDB. diff --git a/doc/conf.py b/doc/conf.py index e87f80a90c..48317aeb16 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -51,7 +51,7 @@ copyright = u'2012 - 2016, The phpMyAdmin devel team' # built documents. # # The short X.Y version. -version = '4.6.5-dev' +version = '4.6.5' # The full version, including alpha/beta/rc tags. release = version diff --git a/libraries/Config.php b/libraries/Config.php index b3dce0e5e7..d5b6a386ab 100644 --- a/libraries/Config.php +++ b/libraries/Config.php @@ -101,7 +101,7 @@ class Config */ public function checkSystem() { - $this->set('PMA_VERSION', '4.6.5-dev'); + $this->set('PMA_VERSION', '4.6.5'); /** * @deprecated */