diff --git a/ChangeLog b/ChangeLog index 4155b78c20..3d24224998 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,12 @@ phpMyAdmin - ChangeLog ====================== +4.9.5 (2020-03-20) +- issue [security] Fix SQL injection with certain usernames (PMASA-2020-2) +- issue [security] Fix SQL injection in particular search situations (PMASA-2020-3) +- issue [security] Fix SQL injection and XSS flaw (PMASA-2020-4) +- issue Deprecate "options" for the external transformation; options must now be hard-coded along with the program name directly in the file. + 4.9.4 (2020-01-07) - issue #15724 Fix 2FA was disabled by a bug - issue [security] Fix SQL injection vulnerability on the user accounts page (PMASA-2020-1) diff --git a/libraries/classes/Controllers/Table/TableSearchController.php b/libraries/classes/Controllers/Table/TableSearchController.php index 90f945f0d4..16ed3fe1fd 100644 --- a/libraries/classes/Controllers/Table/TableSearchController.php +++ b/libraries/classes/Controllers/Table/TableSearchController.php @@ -420,8 +420,8 @@ class TableSearchController extends TableController public function getDataRowAction() { $extra_data = array(); - $row_info_query = 'SELECT * FROM `' . $_POST['db'] . '`.`' - . $_POST['table'] . '` WHERE ' . $_POST['where_clause']; + $row_info_query = 'SELECT * FROM ' . Util::backquote($_POST['db']) . '.' + . Util::backquote($_POST['table']) . ' WHERE ' . $_POST['where_clause']; $result = $this->dbi->query( $row_info_query . ";", DatabaseInterface::CONNECT_USER, diff --git a/libraries/classes/Display/Results.php b/libraries/classes/Display/Results.php index 84de469c86..80120f20c8 100644 --- a/libraries/classes/Display/Results.php +++ b/libraries/classes/Display/Results.php @@ -5163,6 +5163,8 @@ class Results if (count($url_params) > 0 && (!empty($tmpdb) && !empty($meta->orgtable)) ) { + $url_params['where_clause_sign'] = Core::signSqlQuery($url_params['where_clause']); + $result = '' diff --git a/libraries/classes/Error.php b/libraries/classes/Error.php index ded3b4904a..d156f19095 100644 --- a/libraries/classes/Error.php +++ b/libraries/classes/Error.php @@ -467,7 +467,7 @@ class Error extends Message public function isUserError() { return $this->hide_location || - ($this->getNumber() & (E_USER_WARNING | E_USER_ERROR | E_USER_NOTICE)); + ($this->getNumber() & (E_USER_WARNING | E_USER_ERROR | E_USER_NOTICE | E_USER_DEPRECATED)); } /** diff --git a/libraries/classes/ErrorHandler.php b/libraries/classes/ErrorHandler.php index 0c8ad34910..26b51b60ec 100644 --- a/libraries/classes/ErrorHandler.php +++ b/libraries/classes/ErrorHandler.php @@ -166,12 +166,12 @@ class ErrorHandler */ if (error_reporting() == 0 && $this->error_reporting != 0 && - ($errno & (E_USER_WARNING | E_USER_ERROR | E_USER_NOTICE)) == 0 + ($errno & (E_USER_WARNING | E_USER_ERROR | E_USER_NOTICE | E_USER_DEPRECATED)) == 0 ) { return; } } else { - if (($errno & (E_USER_WARNING | E_USER_ERROR | E_USER_NOTICE)) == 0) { + if (($errno & (E_USER_WARNING | E_USER_ERROR | E_USER_NOTICE | E_USER_DEPRECATED)) == 0) { return; } } @@ -229,6 +229,7 @@ class ErrorHandler case E_USER_NOTICE: case E_USER_WARNING: case E_USER_ERROR: + case E_USER_DEPRECATED: // just collect the error // display is called from outside break; diff --git a/libraries/classes/Plugins/Transformations/Abs/ExternalTransformationsPlugin.php b/libraries/classes/Plugins/Transformations/Abs/ExternalTransformationsPlugin.php index d3286aa563..41c2a88a48 100644 --- a/libraries/classes/Plugins/Transformations/Abs/ExternalTransformationsPlugin.php +++ b/libraries/classes/Plugins/Transformations/Abs/ExternalTransformationsPlugin.php @@ -29,10 +29,10 @@ abstract class ExternalTransformationsPlugin extends TransformationsPlugin . ' data via standard input. Returns the standard output of the' . ' application. The default is Tidy, to pretty-print HTML code.' . ' For security reasons, you have to manually edit the file' - . ' libraries/classes/Plugins/Transformations/Output/Text_Plain_External' - . '.php and list the tools you want to make available.' + . ' libraries/classes/Plugins/Transformations/Abs/ExternalTransformationsPlugin.php' + . ' and list the tools you want to make available.' . ' The first option is then the number of the program you want to' - . ' use and the second option is the parameters for the program.' + . ' use. The second option should be blank for historical reasons.' . ' The third option, if set to 1, will convert the output using' . ' htmlspecialchars() (Default 1). The fourth option, if set to 1,' . ' will prevent wrapping and ensure that the output appears all on' @@ -108,6 +108,16 @@ abstract class ExternalTransformationsPlugin extends TransformationsPlugin $program = $allowed_programs[0]; } + if (isset($options[1]) && strlen((string) $options[1]) > 0) { + trigger_error(sprintf( + __( + 'You are using the external transformation command line options field, which has been deprecated for security reasons. ' + . 'Add all command line options directly to the definition in %s.' + ), + '[code]libraries/classes/Plugins/Transformations/Abs/ExternalTransformationsPlugin.php[/code]' + ), E_USER_DEPRECATED); + } + // needs PHP >= 4.3.0 $newstring = ''; $descriptorspec = array( diff --git a/libraries/classes/Server/Privileges.php b/libraries/classes/Server/Privileges.php index 0e3e4a9fa3..a62d111606 100644 --- a/libraries/classes/Server/Privileges.php +++ b/libraries/classes/Server/Privileges.php @@ -1812,8 +1812,11 @@ class Privileges && $mode == 'change' ) { $row = $GLOBALS['dbi']->fetchSingleRow( - 'SELECT `plugin` FROM `mysql`.`user` WHERE ' - . '`User` = "' . $username . '" AND `Host` = "' . $hostname . '" LIMIT 1' + 'SELECT `plugin` FROM `mysql`.`user` WHERE `User` = "' + . $GLOBALS['dbi']->escapeString($username) + . '" AND `Host` = "' + . $GLOBALS['dbi']->escapeString($hostname) + . '" LIMIT 1' ); // Table 'mysql'.'user' may not exist for some previous // versions of MySQL - in that case consider fallback value @@ -1824,8 +1827,11 @@ class Privileges list($username, $hostname) = $GLOBALS['dbi']->getCurrentUserAndHost(); $row = $GLOBALS['dbi']->fetchSingleRow( - 'SELECT `plugin` FROM `mysql`.`user` WHERE ' - . '`User` = "' . $username . '" AND `Host` = "' . $hostname . '"' + 'SELECT `plugin` FROM `mysql`.`user` WHERE `User` = "' + . $GLOBALS['dbi']->escapeString($username) + . '" AND `Host` = "' + . $GLOBALS['dbi']->escapeString($hostname) + . '"' ); if (isset($row) && $row && ! empty($row['plugin'])) { $authentication_plugin = $row['plugin']; @@ -1965,8 +1971,8 @@ class Privileges . " `authentication_string` = '" . $hashedPassword . "', `Password` = '', " . " `plugin` = '" . $authentication_plugin . "'" - . " WHERE `User` = '" . $username . "' AND Host = '" - . $hostname . "';"; + . " WHERE `User` = '" . $GLOBALS['dbi']->escapeString($username) + . "' AND Host = '" . $GLOBALS['dbi']->escapeString($hostname) . "';"; } else { // USE 'SET PASSWORD ...' syntax for rest of the versions // Backup the old value, to be reset later @@ -1976,8 +1982,8 @@ class Privileges $orig_value = $row['@@old_passwords']; $update_plugin_query = "UPDATE `mysql`.`user` SET" . " `plugin` = '" . $authentication_plugin . "'" - . " WHERE `User` = '" . $username . "' AND Host = '" - . $hostname . "';"; + . " WHERE `User` = '" . $GLOBALS['dbi']->escapeString($username) + . "' AND Host = '" . $GLOBALS['dbi']->escapeString($hostname) . "';"; // Update the plugin for the user if (!($GLOBALS['dbi']->tryQuery($update_plugin_query))) { diff --git a/libraries/classes/UserPassword.php b/libraries/classes/UserPassword.php index 585463fe56..be7d2e41a5 100644 --- a/libraries/classes/UserPassword.php +++ b/libraries/classes/UserPassword.php @@ -114,7 +114,8 @@ class UserPassword if ($serverType == 'MySQL' && $serverVersion >= 50706 ) { - $sql_query = 'ALTER USER \'' . $username . '\'@\'' . $hostname + $sql_query = 'ALTER USER \'' . $GLOBALS['dbi']->escapeString($username) + . '\'@\'' . $GLOBALS['dbi']->escapeString($hostname) . '\' IDENTIFIED WITH ' . $orig_auth_plugin . ' BY ' . (($password == '') ? '\'\'' : '\'***\''); } elseif (($serverType == 'MySQL' @@ -182,7 +183,8 @@ class UserPassword $serverVersion = $GLOBALS['dbi']->getVersion(); if ($serverType == 'MySQL' && $serverVersion >= 50706) { - $local_query = 'ALTER USER \'' . $username . '\'@\'' . $hostname . '\'' + $local_query = 'ALTER USER \'' . $GLOBALS['dbi']->escapeString($username) + . '\'@\'' . $GLOBALS['dbi']->escapeString($hostname) . '\'' . ' IDENTIFIED with ' . $orig_auth_plugin . ' BY ' . (($password == '') ? '\'\'' @@ -208,8 +210,8 @@ class UserPassword . " `authentication_string` = '" . $hashedPassword . "', `Password` = '', " . " `plugin` = '" . $orig_auth_plugin . "'" - . " WHERE `User` = '" . $username . "' AND Host = '" - . $hostname . "';"; + . " WHERE `User` = '" . $GLOBALS['dbi']->escapeString($username) + . "' AND Host = '" . $GLOBALS['dbi']->escapeString($hostname) . "';"; } else { $local_query = 'SET password = ' . (($password == '') ? '\'\'' diff --git a/tbl_get_field.php b/tbl_get_field.php index 975102790b..37a75970da 100644 --- a/tbl_get_field.php +++ b/tbl_get_field.php @@ -38,6 +38,15 @@ if (!$GLOBALS['dbi']->getColumns($db, $table)) { PhpMyAdmin\Util::mysqlDie(__('Invalid table name')); } +if (! isset($_GET['where_clause']) + || ! isset($_GET['where_clause_sign']) + || ! Core::checkSqlQuerySignature($_GET['where_clause'], $_GET['where_clause_sign']) +) { +/* l10n: In case a SQL query did not pass a security check */ + Core::fatalError(__('There is an issue with your request.')); + exit; +} + /* Grab data */ $sql = 'SELECT ' . PhpMyAdmin\Util::backquote($_GET['transform_key']) . ' FROM ' . PhpMyAdmin\Util::backquote($table) diff --git a/test/classes/Display/ResultsTest.php b/test/classes/Display/ResultsTest.php index ef2a4fed11..f986883aac 100644 --- a/test/classes/Display/ResultsTest.php +++ b/test/classes/Display/ResultsTest.php @@ -46,6 +46,7 @@ class ResultsTest extends PmaTestCase $GLOBALS['PMA_Config'] = new Config(); $GLOBALS['PMA_Config']->enableBc(); $GLOBALS['text_dir'] = 'ltr'; + $_SESSION[' HMAC_secret '] = 'test'; $dbi = $this->getMockBuilder('PhpMyAdmin\DatabaseInterface') ->disableOriginalConstructor() @@ -1323,7 +1324,7 @@ class ResultsTest extends PmaTestCase $meta = new stdClass(); $meta->type = 'BLOB'; $meta->orgtable = 'bar'; - $url_params = array('db' => 'foo', 'table' => 'bar'); + $url_params = array('db' => 'foo', 'table' => 'bar', 'where_clause' => 'where_clause'); return array( array( @@ -1337,9 +1338,7 @@ class ResultsTest extends PmaTestCase $meta, $url_params, null, - '1001' + 'class="disableAjax">1001' ), array( true, @@ -1352,9 +1351,7 @@ class ResultsTest extends PmaTestCase $meta, $url_params, null, - '0x123456' + 'class="disableAjax">0x123456' ), array( true, @@ -1367,9 +1364,7 @@ class ResultsTest extends PmaTestCase $meta, $url_params, null, - '[BLOB - 4 B]' + 'class="disableAjax">[BLOB - 4 B]' ), array( false, @@ -1431,7 +1426,7 @@ class ResultsTest extends PmaTestCase $_SESSION['tmpval']['display_binary'] = $display_binary; $_SESSION['tmpval']['display_blob'] = $display_blob; $GLOBALS['cfg']['LimitChars'] = 50; - $this->assertEquals( + $this->assertContains( $output, $this->_callPrivateFunction( '_handleNonPrintableContents', @@ -1471,7 +1466,7 @@ class ResultsTest extends PmaTestCase $meta2->decimals = 0; $meta2->name = 'varchar'; $meta2->orgname = 'varchar'; - $url_params = array('db' => 'foo', 'table' => 'tbl'); + $url_params = array('db' => 'foo', 'table' => 'tbl', 'where_clause' => 'where_clause'); return array( array( @@ -1489,11 +1484,7 @@ class ResultsTest extends PmaTestCase array(), 0, 'binary', - '' . PHP_EOL - . ' [BLOB - 4 B]' . PHP_EOL + 'class="disableAjax">[BLOB - 4 B]' . PHP_EOL . '' . PHP_EOL ), array( @@ -1596,7 +1587,7 @@ class ResultsTest extends PmaTestCase $_SESSION['tmpval']['relational_display'] = false; $GLOBALS['cfg']['LimitChars'] = 50; $GLOBALS['cfg']['ProtectBinary'] = $protectBinary; - $this->assertEquals( + $this->assertContains( $output, $this->_callPrivateFunction( '_getDataCellForNonNumericColumns',