diff --git a/libraries/classes/Core.php b/libraries/classes/Core.php index bf5edde41a..98ea2031f6 100644 --- a/libraries/classes/Core.php +++ b/libraries/classes/Core.php @@ -1317,4 +1317,27 @@ class Core $hmac = hash_hmac('sha256', $sqlQuery, $_SESSION[' HMAC_secret '] . $cfg['blowfish_secret']); return hash_equals($hmac, $signature); } + + /** + * @return void + */ + public static function populateRequestWithEncryptedQueryParams() + { + if (! isset($_GET['eq']) || ! is_string($_GET['eq'])) { + unset($_GET['eq'], $_REQUEST['eq']); + return; + } + + $decryptedQuery = Url::decryptQuery($_GET['eq']); + unset($_GET['eq'], $_REQUEST['eq']); + if ($decryptedQuery === null) { + return; + } + + $urlQueryParams = (array) json_decode($decryptedQuery); + foreach ($urlQueryParams as $urlQueryParamKey => $urlQueryParamValue) { + $_GET[$urlQueryParamKey] = $urlQueryParamValue; + $_REQUEST[$urlQueryParamKey] = $urlQueryParamValue; + } + } } diff --git a/libraries/classes/Crypto/Crypto.php b/libraries/classes/Crypto/Crypto.php index 5f12fbdcae..6449f7dca5 100644 --- a/libraries/classes/Crypto/Crypto.php +++ b/libraries/classes/Crypto/Crypto.php @@ -13,10 +13,14 @@ final class Crypto /** @var bool */ private $hasSodiumSupport; - public function __construct() + /** + * @param bool $forceFallback Force the usage of the fallback functions. + */ + public function __construct($forceFallback = false) { - $this->hasRandomBytesSupport = is_callable('random_bytes'); - $this->hasSodiumSupport = $this->hasRandomBytesSupport + $this->hasRandomBytesSupport = ! $forceFallback && is_callable('random_bytes'); + $this->hasSodiumSupport = ! $forceFallback + && $this->hasRandomBytesSupport && is_callable('sodium_crypto_secretbox') && is_callable('sodium_crypto_secretbox_open') && defined('SODIUM_CRYPTO_SECRETBOX_NONCEBYTES') diff --git a/libraries/classes/Navigation/NavigationTree.php b/libraries/classes/Navigation/NavigationTree.php index c40b1408b9..3d37577d95 100644 --- a/libraries/classes/Navigation/NavigationTree.php +++ b/libraries/classes/Navigation/NavigationTree.php @@ -1574,6 +1574,6 @@ class NavigationTree $url = parse_url($link); parse_str(htmlspecialchars_decode($url['query']), $query); - return $url['path'] . '?' . Url::buildHttpQuery($query); + return $url['path'] . '?' . htmlspecialchars(Url::buildHttpQuery($query)); } } diff --git a/libraries/classes/Url.php b/libraries/classes/Url.php index 215a1b98fc..cd1f89a4c0 100644 --- a/libraries/classes/Url.php +++ b/libraries/classes/Url.php @@ -270,7 +270,9 @@ class Url unset($data[$paramKey]); } - $data['eq'] = self::encryptQuery(json_encode($paramsToEncrypt)); + if ($paramsToEncrypt !== []) { + $data['eq'] = self::encryptQuery(json_encode($paramsToEncrypt)); + } return http_build_query($data, null, $separator); } diff --git a/libraries/common.inc.php b/libraries/common.inc.php index 0277c8af55..6705fd08be 100644 --- a/libraries/common.inc.php +++ b/libraries/common.inc.php @@ -43,7 +43,6 @@ use PhpMyAdmin\Response; use PhpMyAdmin\Session; use PhpMyAdmin\ThemeManager; use PhpMyAdmin\Tracker; -use PhpMyAdmin\Url; use PhpMyAdmin\Util; /** @@ -133,16 +132,7 @@ if (! defined('PMA_NO_SESSION')) { Session::setUp($GLOBALS['PMA_Config'], $GLOBALS['error_handler']); } -if (isset($_GET['eq']) && is_string($_GET['eq'])) { - $decryptedQuery = Url::decryptQuery($_GET['eq']); - if ($decryptedQuery !== null) { - $urlQueryParams = json_decode($decryptedQuery); - foreach ($urlQueryParams as $urlQueryParamKey => $urlQueryParamValue) { - $_GET[$urlQueryParamKey] = $urlQueryParamValue; - $_REQUEST[$urlQueryParamKey] = $urlQueryParamValue; - } - } -} +Core::populateRequestWithEncryptedQueryParams(); /** * init some variables LABEL_variables_init diff --git a/test/classes/CoreTest.php b/test/classes/CoreTest.php index 11e29fb90f..87a1525c3f 100644 --- a/test/classes/CoreTest.php +++ b/test/classes/CoreTest.php @@ -11,6 +11,7 @@ use PhpMyAdmin\Config; use PhpMyAdmin\Core; use PhpMyAdmin\Sanitize; use PhpMyAdmin\Tests\PmaTestCase; +use PhpMyAdmin\Url; use stdClass; /** @@ -50,6 +51,7 @@ class CoreTest extends PmaTestCase $GLOBALS['db'] = ''; $GLOBALS['table'] = ''; $GLOBALS['PMA_PHP_SELF'] = 'http://example.net/'; + $GLOBALS['PMA_Config']->set('URLQueryEncryption', false); } /** @@ -1226,4 +1228,61 @@ class CoreTest extends PmaTestCase // Must work now, (good secret and blowfish_secret) $this->assertTrue(Core::checkSqlQuerySignature($sqlQuery, $hmac)); } + + /** + * @return void + */ + public function testPopulateRequestWithEncryptedQueryParams() + { + global $PMA_Config; + + $_SESSION = []; + $PMA_Config->set('URLQueryEncryption', true); + $PMA_Config->set('URLQueryEncryptionSecretKey', str_repeat('a', 32)); + + $_GET = ['pos' => '0', 'eq' => Url::encryptQuery('{"db":"test_db","table":"test_table"}')]; + $_REQUEST = $_GET; + + Core::populateRequestWithEncryptedQueryParams(); + + $expected = ['pos' => '0', 'db' => 'test_db', 'table' => 'test_table']; + + $this->assertEquals($expected, $_GET); + $this->assertEquals($expected, $_REQUEST); + } + + /** + * @return void + * + * @dataProvider providerForTestPopulateRequestWithEncryptedQueryParamsWithInvalidParam + */ + public function testPopulateRequestWithEncryptedQueryParamsWithInvalidParam($encrypted, $decrypted) + { + global $PMA_Config; + + $_SESSION = []; + $PMA_Config->set('URLQueryEncryption', true); + $PMA_Config->set('URLQueryEncryptionSecretKey', str_repeat('a', 32)); + + $_GET = $encrypted; + $_REQUEST = $encrypted; + + Core::populateRequestWithEncryptedQueryParams(); + + $this->assertEquals($decrypted, $_GET); + $this->assertEquals($decrypted, $_REQUEST); + } + + /** + * @return string[][][] + */ + public function providerForTestPopulateRequestWithEncryptedQueryParamsWithInvalidParam() + { + return [ + [[], []], + [['eq' => []], []], + [['eq' => ''], []], + [['eq' => 'invalid'], []], + ]; + } } diff --git a/test/classes/Crypto/CryptoTest.php b/test/classes/Crypto/CryptoTest.php new file mode 100644 index 0000000000..a73260d07b --- /dev/null +++ b/test/classes/Crypto/CryptoTest.php @@ -0,0 +1,114 @@ +set('URLQueryEncryptionSecretKey', str_repeat('a', 32)); + + $cryptoWithSodium = new Crypto(); + $encrypted = $cryptoWithSodium->encrypt('test'); + $this->assertNotSame('test', $encrypted); + $this->assertSame('test', $cryptoWithSodium->decrypt($encrypted)); + $this->assertArrayNotHasKey('URLQueryEncryptionSecretKey', $_SESSION); + + $cryptoWithPhpseclib = new Crypto(true); + $encrypted = $cryptoWithPhpseclib->encrypt('test'); + $this->assertNotSame('test', $encrypted); + $this->assertSame('test', $cryptoWithPhpseclib->decrypt($encrypted)); + $this->assertArrayNotHasKey('URLQueryEncryptionSecretKey', $_SESSION); + } + + /** + * @return void + */ + public function testWithValidKeyFromSession() + { + global $PMA_Config; + + $_SESSION = ['URLQueryEncryptionSecretKey' => str_repeat('a', 32)]; + $PMA_Config->set('URLQueryEncryptionSecretKey', ''); + + $cryptoWithSodium = new Crypto(); + $encrypted = $cryptoWithSodium->encrypt('test'); + $this->assertNotSame('test', $encrypted); + $this->assertSame('test', $cryptoWithSodium->decrypt($encrypted)); + $this->assertArrayHasKey('URLQueryEncryptionSecretKey', $_SESSION); + + $cryptoWithPhpseclib = new Crypto(true); + $encrypted = $cryptoWithPhpseclib->encrypt('test'); + $this->assertNotSame('test', $encrypted); + $this->assertSame('test', $cryptoWithPhpseclib->decrypt($encrypted)); + $this->assertArrayHasKey('URLQueryEncryptionSecretKey', $_SESSION); + } + + /** + * @return void + */ + public function testWithNewSessionKey() + { + global $PMA_Config; + + $_SESSION = []; + $PMA_Config->set('URLQueryEncryptionSecretKey', ''); + + $cryptoWithSodium = new Crypto(); + $encrypted = $cryptoWithSodium->encrypt('test'); + $this->assertNotSame('test', $encrypted); + $this->assertSame('test', $cryptoWithSodium->decrypt($encrypted)); + $this->assertArrayHasKey('URLQueryEncryptionSecretKey', $_SESSION); + $this->assertEquals(32, mb_strlen($_SESSION['URLQueryEncryptionSecretKey'], '8bit')); + + $cryptoWithPhpseclib = new Crypto(true); + $encrypted = $cryptoWithPhpseclib->encrypt('test'); + $this->assertNotSame('test', $encrypted); + $this->assertSame('test', $cryptoWithPhpseclib->decrypt($encrypted)); + $this->assertArrayHasKey('URLQueryEncryptionSecretKey', $_SESSION); + $this->assertEquals(32, mb_strlen($_SESSION['URLQueryEncryptionSecretKey'], '8bit')); + } + + /** + * @return void + */ + public function testDecryptWithInvalidKey() + { + global $PMA_Config; + + $_SESSION = []; + $PMA_Config->set('URLQueryEncryptionSecretKey', str_repeat('a', 32)); + + $cryptoWithSodium = new Crypto(); + $encrypted = $cryptoWithSodium->encrypt('test'); + $this->assertNotSame('test', $encrypted); + $this->assertSame('test', $cryptoWithSodium->decrypt($encrypted)); + + $PMA_Config->set('URLQueryEncryptionSecretKey', str_repeat('b', 32)); + + $cryptoWithSodium = new Crypto(); + $this->assertNull($cryptoWithSodium->decrypt($encrypted)); + + $cryptoWithPhpseclib = new Crypto(true); + $encrypted = $cryptoWithPhpseclib->encrypt('test'); + $this->assertNotSame('test', $encrypted); + $this->assertSame('test', $cryptoWithPhpseclib->decrypt($encrypted)); + + $PMA_Config->set('URLQueryEncryptionSecretKey', str_repeat('a', 32)); + + $cryptoWithPhpseclib = new Crypto(true); + $this->assertNull($cryptoWithPhpseclib->decrypt($encrypted)); + } +} diff --git a/test/classes/Navigation/NavigationTreeTest.php b/test/classes/Navigation/NavigationTreeTest.php index f391db0459..963019fb8a 100644 --- a/test/classes/Navigation/NavigationTreeTest.php +++ b/test/classes/Navigation/NavigationTreeTest.php @@ -11,6 +11,8 @@ use PhpMyAdmin\Config; use PhpMyAdmin\Navigation\NavigationTree; use PhpMyAdmin\Tests\PmaTestCase; use PhpMyAdmin\Theme; +use PhpMyAdmin\Url; +use ReflectionMethod; /* * we must set $GLOBALS['server'] here @@ -101,4 +103,37 @@ class NavigationTreeTest extends PmaTestCase $result = $this->object->renderDbSelect(); $this->assertContains('pma_navigation_select_database', $result); } + + /** + * @return void + */ + public function testEncryptQueryParams() + { + global $PMA_Config; + + $_SESSION = []; + $PMA_Config->set('URLQueryEncryption', false); + $PMA_Config->set('URLQueryEncryptionSecretKey', str_repeat('a', 32)); + + $method = new ReflectionMethod($this->object, 'encryptQueryParams'); + $method->setAccessible(true); + + $link = 'tbl_structure.php?server=1&db=test_db&table=test_table&pos=0'; + + $actual = $method->invoke($this->object, $link); + $this->assertEquals($link, $actual); + + $PMA_Config->set('URLQueryEncryption', true); + + $actual = $method->invoke($this->object, $link); + $this->assertStringStartsWith('tbl_structure.php?server=1&pos=0&eq=', $actual); + + $url = parse_url($actual); + parse_str(htmlspecialchars_decode($url['query']), $query); + + $this->assertRegExp('/^[a-zA-Z0-9-_=]+$/', $query['eq']); + $decrypted = Url::decryptQuery($query['eq']); + $this->assertJson($decrypted); + $this->assertSame('{"db":"test_db","table":"test_table"}', $decrypted); + } } diff --git a/test/classes/UrlTest.php b/test/classes/UrlTest.php index 2bce450315..737e54aa41 100644 --- a/test/classes/UrlTest.php +++ b/test/classes/UrlTest.php @@ -27,6 +27,7 @@ class UrlTest extends TestCase public function setUp() { unset($_COOKIE['pma_lang']); + $GLOBALS['PMA_Config']->set('URLQueryEncryption', false); } /** @@ -105,4 +106,61 @@ class UrlTest extends TestCase $expected = '?server=x' . htmlentities($separator) . 'lang=en' ; $this->assertEquals($expected, Url::getCommon()); } + + /** + * @return void + */ + public function testBuildHttpQueryWithUrlQueryEncryptionDisabled() + { + global $PMA_Config; + + $PMA_Config->set('URLQueryEncryption', false); + $params = ['db' => 'test_db', 'table' => 'test_table', 'pos' => 0]; + $this->assertEquals('db=test_db&table=test_table&pos=0', Url::buildHttpQuery($params)); + } + + /** + * @return void + */ + public function testBuildHttpQueryWithUrlQueryEncryptionEnabled() + { + global $PMA_Config; + + $_SESSION = []; + $PMA_Config->set('URLQueryEncryption', true); + $PMA_Config->set('URLQueryEncryptionSecretKey', str_repeat('a', 32)); + + $params = ['db' => 'test_db', 'table' => 'test_table', 'pos' => 0]; + $query = Url::buildHttpQuery($params); + $this->assertStringStartsWith('pos=0&eq=', $query); + parse_str($query, $queryParams); + $this->assertCount(2, $queryParams); + $this->assertSame('0', $queryParams['pos']); + $this->assertTrue(is_string($queryParams['eq'])); + $this->assertNotSame('', $queryParams['eq']); + $this->assertRegExp('/^[a-zA-Z0-9-_=]+$/', $queryParams['eq']); + $decrypted = Url::decryptQuery($queryParams['eq']); + $this->assertJson($decrypted); + $this->assertSame('{"db":"test_db","table":"test_table"}', $decrypted); + } + + /** + * @return void + */ + public function testQueryEncryption() + { + global $PMA_Config; + + $_SESSION = []; + $PMA_Config->set('URLQueryEncryption', true); + $PMA_Config->set('URLQueryEncryptionSecretKey', str_repeat('a', 32)); + + $query = '{"db":"test_db","table":"test_table"}'; + $encrypted = Url::encryptQuery($query); + $this->assertNotSame($query, $encrypted); + $this->assertNotSame('', $encrypted); + $this->assertRegExp('/^[a-zA-Z0-9-_=]+$/', $encrypted); + $decrypted = Url::decryptQuery($encrypted); + $this->assertSame($query, $decrypted); + } }