phpmyadmin/test/classes/Plugins/Auth/AuthenticationCookieTest.php
William Desportes cf6f5d5055
Merge #16406 - Fix #16457 - Add config parameters to support third-party reCAPTCHA v2 compatible APIs
Fixes: #16457
Pull-request: #16406

Signed-off-by: William Desportes <williamdes@wdes.fr>
2020-11-05 00:04:29 +01:00

1415 lines
42 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace PhpMyAdmin\Tests\Plugins\Auth;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\ErrorHandler;
use PhpMyAdmin\Footer;
use PhpMyAdmin\Header;
use PhpMyAdmin\Plugins\Auth\AuthenticationCookie;
use PhpMyAdmin\Tests\AbstractNetworkTestCase;
use ReflectionException;
use ReflectionMethod;
use function base64_encode;
use function function_exists;
use function is_readable;
use function json_encode;
use function ob_get_clean;
use function ob_start;
use function str_repeat;
use function str_shuffle;
use function strlen;
use function time;
class AuthenticationCookieTest extends AbstractNetworkTestCase
{
/** @var AuthenticationCookie */
protected $object;
/**
* Configures global environment.
*/
protected function setUp(): void
{
parent::setUp();
parent::defineVersionConstants();
parent::setLanguage();
parent::setTheme();
parent::setGlobalConfig();
$GLOBALS['PMA_Config']->enableBc();
$GLOBALS['server'] = 0;
$GLOBALS['text_dir'] = 'ltr';
$GLOBALS['db'] = 'db';
$GLOBALS['table'] = 'table';
$_POST['pma_password'] = '';
$this->object = new AuthenticationCookie();
$GLOBALS['PMA_PHP_SELF'] = '/phpmyadmin/';
$GLOBALS['cfg']['Server']['DisableIS'] = false;
}
/**
* tearDown for test cases
*/
protected function tearDown(): void
{
parent::tearDown();
unset($this->object);
}
/**
* @group medium
*/
public function testAuthErrorAJAX(): void
{
$mockResponse = $this->mockResponse();
$mockResponse->expects($this->once())
->method('isAjax')
->with()
->will($this->returnValue(true));
$mockResponse->expects($this->once())
->method('setRequestStatus')
->with(false);
$mockResponse->expects($this->once())
->method('addJSON')
->with(
'redirect_flag',
'1'
);
$GLOBALS['conn_error'] = true;
$this->assertTrue(
$this->object->showLoginForm()
);
}
private function getAuthErrorMockResponse(): void
{
$mockResponse = $this->mockResponse();
$mockResponse->expects($this->once())
->method('isAjax')
->with()
->will($this->returnValue(false));
// mock footer
$mockFooter = $this->getMockBuilder(Footer::class)
->disableOriginalConstructor()
->setMethods(['setMinimal'])
->getMock();
$mockFooter->expects($this->once())
->method('setMinimal')
->with();
// mock header
$mockHeader = $this->getMockBuilder(Header::class)
->disableOriginalConstructor()
->setMethods(
[
'setBodyId',
'setTitle',
'disableMenuAndConsole',
'disableWarnings',
]
)
->getMock();
$mockHeader->expects($this->once())
->method('setBodyId')
->with('loginform');
$mockHeader->expects($this->once())
->method('setTitle')
->with('phpMyAdmin');
$mockHeader->expects($this->once())
->method('disableMenuAndConsole')
->with();
$mockHeader->expects($this->once())
->method('disableWarnings')
->with();
// set mocked headers and footers
$mockResponse->expects($this->once())
->method('getFooter')
->with()
->will($this->returnValue($mockFooter));
$mockResponse->expects($this->once())
->method('getHeader')
->with()
->will($this->returnValue($mockHeader));
$GLOBALS['cfg']['Servers'] = [
1,
2,
];
// mock error handler
$mockErrorHandler = $this->getMockBuilder(ErrorHandler::class)
->disableOriginalConstructor()
->setMethods(['hasDisplayErrors'])
->getMock();
$mockErrorHandler->expects($this->once())
->method('hasDisplayErrors')
->with()
->will($this->returnValue(true));
$GLOBALS['error_handler'] = $mockErrorHandler;
}
/**
* @group medium
*/
public function testAuthError(): void
{
$_REQUEST['old_usr'] = '';
$GLOBALS['cfg']['LoginCookieRecall'] = true;
$GLOBALS['cfg']['blowfish_secret'] = 'secret';
$this->object->user = 'pmauser';
$GLOBALS['pma_auth_server'] = 'localhost';
$GLOBALS['conn_error'] = true;
$GLOBALS['cfg']['Lang'] = 'en';
$GLOBALS['cfg']['AllowArbitraryServer'] = true;
$GLOBALS['cfg']['CaptchaApi'] = '';
$GLOBALS['cfg']['CaptchaRequestParam'] = '';
$GLOBALS['cfg']['CaptchaResponseParam'] = '';
$GLOBALS['cfg']['CaptchaLoginPrivateKey'] = '';
$GLOBALS['cfg']['CaptchaLoginPublicKey'] = '';
$GLOBALS['db'] = 'testDb';
$GLOBALS['table'] = 'testTable';
$GLOBALS['cfg']['Servers'] = [1, 2];
$GLOBALS['error_handler'] = new ErrorHandler();
ob_start();
$this->object->showLoginForm();
$result = ob_get_clean();
$this->assertIsString($result);
$this->assertStringContainsString(
' id="imLogo"',
$result
);
$this->assertStringContainsString(
'<div class="alert alert-danger" role="alert">',
$result
);
$this->assertStringContainsString(
'<form method="post" id="login_form" action="index.php?route=/" name="login_form" ' .
'class="disableAjax hide login js-show form-horizontal">',
$result
);
$this->assertStringContainsString(
'<input type="text" name="pma_servername" id="input_servername" ' .
'value="localhost"',
$result
);
$this->assertStringContainsString(
'<input type="text" name="pma_username" id="input_username" ' .
'value="pmauser" size="24" class="textfield" autocomplete="username">',
$result
);
$this->assertStringContainsString(
'<input type="password" name="pma_password" id="input_password" ' .
'value="" size="24" class="textfield" autocomplete="current-password">',
$result
);
$this->assertStringContainsString(
'<select name="server" id="select_server" ' .
'onchange="document.forms[\'login_form\'].' .
'elements[\'pma_servername\'].value = \'\'">',
$result
);
$this->assertStringContainsString(
'<input type="hidden" name="db" value="testDb">',
$result
);
$this->assertStringContainsString(
'<input type="hidden" name="table" value="testTable">',
$result
);
}
/**
* @group medium
*/
public function testAuthCaptcha(): void
{
$mockResponse = $this->mockResponse();
$mockResponse->expects($this->once())
->method('isAjax')
->with()
->will($this->returnValue(false));
$mockResponse->expects($this->once())
->method('getFooter')
->with()
->will($this->returnValue(new Footer()));
$mockResponse->expects($this->once())
->method('getHeader')
->with()
->will($this->returnValue(new Header()));
$_REQUEST['old_usr'] = '';
$GLOBALS['cfg']['LoginCookieRecall'] = false;
$GLOBALS['cfg']['Lang'] = '';
$GLOBALS['cfg']['AllowArbitraryServer'] = false;
$GLOBALS['cfg']['Servers'] = [1];
$GLOBALS['cfg']['CaptchaApi'] = 'https://www.google.com/recaptcha/api.js';
$GLOBALS['cfg']['CaptchaRequestParam'] = 'g-recaptcha';
$GLOBALS['cfg']['CaptchaResponseParam'] = 'g-recaptcha-response';
$GLOBALS['cfg']['CaptchaLoginPrivateKey'] = 'testprivkey';
$GLOBALS['cfg']['CaptchaLoginPublicKey'] = 'testpubkey';
$GLOBALS['server'] = 0;
$GLOBALS['error_handler'] = new ErrorHandler();
ob_start();
$this->object->showLoginForm();
$result = ob_get_clean();
$this->assertIsString($result);
$this->assertStringContainsString('id="imLogo"', $result);
// Check for language selection if locales are there
$loc = LOCALE_PATH . '/cs/LC_MESSAGES/phpmyadmin.mo';
if (is_readable($loc)) {
$this->assertStringContainsString(
'<select name="lang" class="autosubmit" lang="en" dir="ltr" ' .
'id="sel-lang">',
$result
);
}
$this->assertStringContainsString(
'<form method="post" id="login_form" action="index.php?route=/" name="login_form"' .
' class="disableAjax hide login js-show form-horizontal" autocomplete="off">',
$result
);
$this->assertStringContainsString(
'<input type="hidden" name="server" value="0">',
$result
);
$this->assertStringContainsString(
'<script src="https://www.google.com/recaptcha/api.js?hl=en"'
. ' async defer></script>',
$result
);
$this->assertStringContainsString(
'<input class="btn btn-primary g-recaptcha" data-sitekey="testpubkey"'
. ' data-callback="Functions_recaptchaCallback" value="Go" type="submit" id="input_go">',
$result
);
}
/**
* @group medium
*/
public function testAuthCaptchaCheckbox(): void
{
$mockResponse = $this->mockResponse();
$mockResponse->expects($this->once())
->method('isAjax')
->with()
->will($this->returnValue(false));
$mockResponse->expects($this->once())
->method('getFooter')
->with()
->will($this->returnValue(new Footer()));
$mockResponse->expects($this->once())
->method('getHeader')
->with()
->will($this->returnValue(new Header()));
$_REQUEST['old_usr'] = '';
$GLOBALS['cfg']['LoginCookieRecall'] = false;
$GLOBALS['cfg']['Lang'] = '';
$GLOBALS['cfg']['AllowArbitraryServer'] = false;
$GLOBALS['cfg']['Servers'] = [1];
$GLOBALS['cfg']['CaptchaApi'] = 'https://www.google.com/recaptcha/api.js';
$GLOBALS['cfg']['CaptchaRequestParam'] = 'g-recaptcha';
$GLOBALS['cfg']['CaptchaResponseParam'] = 'g-recaptcha-response';
$GLOBALS['cfg']['CaptchaLoginPrivateKey'] = 'testprivkey';
$GLOBALS['cfg']['CaptchaLoginPublicKey'] = 'testpubkey';
$GLOBALS['cfg']['CaptchaMethod'] = 'checkbox';
$GLOBALS['server'] = 0;
$GLOBALS['error_handler'] = new ErrorHandler();
ob_start();
$this->object->showLoginForm();
$result = ob_get_clean();
$this->assertIsString($result);
$this->assertStringContainsString('id="imLogo"', $result);
// Check for language selection if locales are there
$loc = LOCALE_PATH . '/cs/LC_MESSAGES/phpmyadmin.mo';
if (is_readable($loc)) {
$this->assertStringContainsString(
'<select name="lang" class="autosubmit" lang="en" dir="ltr" ' .
'id="sel-lang">',
$result
);
}
$this->assertStringContainsString(
'<form method="post" id="login_form" action="index.php?route=/" name="login_form"' .
' class="disableAjax hide login js-show form-horizontal" autocomplete="off">',
$result
);
$this->assertStringContainsString(
'<input type="hidden" name="server" value="0">',
$result
);
$this->assertStringContainsString(
'<script src="https://www.google.com/recaptcha/api.js?hl=en"'
. ' async defer></script>',
$result
);
$this->assertStringContainsString(
'<div class="g-recaptcha" data-sitekey="testpubkey"></div>',
$result
);
$this->assertStringContainsString(
'<input class="btn btn-primary" value="Go" type="submit" id="input_go">',
$result
);
}
public function testAuthHeader(): void
{
$GLOBALS['cfg']['LoginCookieDeleteAll'] = false;
$GLOBALS['cfg']['Servers'] = [1];
$this->mockResponse('Location: https://example.com/logout');
$GLOBALS['cfg']['Server']['LogoutURL'] = 'https://example.com/logout';
$GLOBALS['cfg']['Server']['auth_type'] = 'cookie';
$this->object->logOut();
}
public function testAuthHeaderPartial(): void
{
$GLOBALS['PMA_Config']->set('is_https', false);
$GLOBALS['cfg']['LoginCookieDeleteAll'] = false;
$GLOBALS['cfg']['Servers'] = [
1,
2,
3,
];
$GLOBALS['cfg']['Server']['LogoutURL'] = 'https://example.com/logout';
$GLOBALS['cfg']['Server']['auth_type'] = 'cookie';
$_COOKIE['pmaAuth-2'] = '';
$this->mockResponse('Location: /phpmyadmin/index.php?route=/&server=2&lang=en');
$this->object->logOut();
}
public function testAuthCheckCaptcha(): void
{
$GLOBALS['cfg']['CaptchaApi'] = 'https://www.google.com/recaptcha/api.js';
$GLOBALS['cfg']['CaptchaRequestParam'] = 'g-recaptcha';
$GLOBALS['cfg']['CaptchaResponseParam'] = 'g-recaptcha-response';
$GLOBALS['cfg']['CaptchaLoginPrivateKey'] = 'testprivkey';
$GLOBALS['cfg']['CaptchaLoginPublicKey'] = 'testpubkey';
$_POST['g-recaptcha-response'] = '';
$_POST['pma_username'] = 'testPMAUser';
$this->assertFalse(
$this->object->readCredentials()
);
$this->assertEquals(
'Missing reCAPTCHA verification, maybe it has been blocked by adblock?',
$GLOBALS['conn_error']
);
}
public function testLogoutDelete(): void
{
$this->mockResponse('Location: /phpmyadmin/index.php?route=/');
$GLOBALS['cfg']['CaptchaApi'] = '';
$GLOBALS['cfg']['CaptchaRequestParam'] = '';
$GLOBALS['cfg']['CaptchaResponseParam'] = '';
$GLOBALS['cfg']['CaptchaLoginPrivateKey'] = '';
$GLOBALS['cfg']['CaptchaLoginPublicKey'] = '';
$GLOBALS['cfg']['LoginCookieDeleteAll'] = true;
$GLOBALS['PMA_Config']->set('PmaAbsoluteUri', '');
$GLOBALS['PMA_Config']->set('is_https', false);
$GLOBALS['cfg']['Servers'] = [1];
$_COOKIE['pmaAuth-0'] = 'test';
$this->object->logOut();
$this->assertArrayNotHasKey(
'pmaAuth-0',
$_COOKIE
);
}
public function testLogout(): void
{
$this->mockResponse('Location: /phpmyadmin/index.php?route=/');
$GLOBALS['cfg']['CaptchaApi'] = '';
$GLOBALS['cfg']['CaptchaRequestParam'] = '';
$GLOBALS['cfg']['CaptchaResponseParam'] = '';
$GLOBALS['cfg']['CaptchaLoginPrivateKey'] = '';
$GLOBALS['cfg']['CaptchaLoginPublicKey'] = '';
$GLOBALS['cfg']['LoginCookieDeleteAll'] = false;
$GLOBALS['PMA_Config']->set('PmaAbsoluteUri', '');
$GLOBALS['PMA_Config']->set('is_https', false);
$GLOBALS['cfg']['Servers'] = [1];
$GLOBALS['server'] = 1;
$GLOBALS['cfg']['Server'] = ['auth_type' => 'cookie'];
$_COOKIE['pmaAuth-1'] = 'test';
$this->object->logOut();
$this->assertArrayNotHasKey(
'pmaAuth-1',
$_COOKIE
);
}
public function testAuthCheckArbitrary(): void
{
$GLOBALS['cfg']['CaptchaApi'] = '';
$GLOBALS['cfg']['CaptchaRequestParam'] = '';
$GLOBALS['cfg']['CaptchaResponseParam'] = '';
$GLOBALS['cfg']['CaptchaLoginPrivateKey'] = '';
$GLOBALS['cfg']['CaptchaLoginPublicKey'] = '';
$_REQUEST['old_usr'] = '';
$_POST['pma_username'] = 'testPMAUser';
$_REQUEST['pma_servername'] = 'testPMAServer';
$_POST['pma_password'] = 'testPMAPSWD';
$GLOBALS['cfg']['AllowArbitraryServer'] = true;
$this->assertTrue(
$this->object->readCredentials()
);
$this->assertEquals(
'testPMAUser',
$this->object->user
);
$this->assertEquals(
'testPMAPSWD',
$this->object->password
);
$this->assertEquals(
'testPMAServer',
$GLOBALS['pma_auth_server']
);
$this->assertArrayNotHasKey(
'pmaAuth-1',
$_COOKIE
);
}
public function testAuthCheckInvalidCookie(): void
{
$GLOBALS['cfg']['AllowArbitraryServer'] = true;
$_REQUEST['pma_servername'] = 'testPMAServer';
$_POST['pma_password'] = 'testPMAPSWD';
$_POST['pma_username'] = '';
$GLOBALS['server'] = 1;
$_COOKIE['pmaUser-1'] = '';
$_COOKIE['pma_iv-1'] = base64_encode('testiv09testiv09');
$this->assertFalse(
$this->object->readCredentials()
);
}
public function testAuthCheckExpires(): void
{
$GLOBALS['server'] = 1;
$_COOKIE['pmaServer-1'] = 'pmaServ1';
$_COOKIE['pmaUser-1'] = 'pmaUser1';
$_COOKIE['pma_iv-1'] = base64_encode('testiv09testiv09');
$_COOKIE['pmaAuth-1'] = '';
$GLOBALS['cfg']['blowfish_secret'] = 'secret';
$_SESSION['last_access_time'] = time() - 1000;
$GLOBALS['cfg']['LoginCookieValidity'] = 1440;
$this->assertFalse(
$this->object->readCredentials()
);
}
public function testAuthCheckDecryptUser(): void
{
$GLOBALS['server'] = 1;
$_REQUEST['old_usr'] = '';
$_POST['pma_username'] = '';
$_COOKIE['pmaServer-1'] = 'pmaServ1';
$_COOKIE['pmaUser-1'] = 'pmaUser1';
$_COOKIE['pma_iv-1'] = base64_encode('testiv09testiv09');
$GLOBALS['cfg']['blowfish_secret'] = 'secret';
$_SESSION['last_access_time'] = '';
$GLOBALS['cfg']['CaptchaApi'] = '';
$GLOBALS['cfg']['CaptchaRequestParam'] = '';
$GLOBALS['cfg']['CaptchaResponseParam'] = '';
$GLOBALS['cfg']['CaptchaLoginPrivateKey'] = '';
$GLOBALS['cfg']['CaptchaLoginPublicKey'] = '';
$GLOBALS['PMA_Config']->set('is_https', false);
// mock for blowfish function
$this->object = $this->getMockBuilder(AuthenticationCookie::class)
->disableOriginalConstructor()
->setMethods(['cookieDecrypt'])
->getMock();
$this->object->expects($this->once())
->method('cookieDecrypt')
->will($this->returnValue('testBF'));
$this->assertFalse(
$this->object->readCredentials()
);
$this->assertEquals(
'testBF',
$this->object->user
);
}
public function testAuthCheckDecryptPassword(): void
{
$GLOBALS['server'] = 1;
$_REQUEST['old_usr'] = '';
$_POST['pma_username'] = '';
$_COOKIE['pmaServer-1'] = 'pmaServ1';
$_COOKIE['pmaUser-1'] = 'pmaUser1';
$_COOKIE['pmaAuth-1'] = 'pmaAuth1';
$_COOKIE['pma_iv-1'] = base64_encode('testiv09testiv09');
$GLOBALS['cfg']['blowfish_secret'] = 'secret';
$GLOBALS['cfg']['CaptchaApi'] = '';
$GLOBALS['cfg']['CaptchaRequestParam'] = '';
$GLOBALS['cfg']['CaptchaResponseParam'] = '';
$GLOBALS['cfg']['CaptchaLoginPrivateKey'] = '';
$GLOBALS['cfg']['CaptchaLoginPublicKey'] = '';
$_SESSION['browser_access_time']['default'] = time() - 1000;
$GLOBALS['cfg']['LoginCookieValidity'] = 1440;
$GLOBALS['PMA_Config']->set('is_https', false);
// mock for blowfish function
$this->object = $this->getMockBuilder(AuthenticationCookie::class)
->disableOriginalConstructor()
->setMethods(['cookieDecrypt'])
->getMock();
$this->object->expects($this->at(1))
->method('cookieDecrypt')
->will($this->returnValue('{"password":""}'));
$this->assertTrue(
$this->object->readCredentials()
);
$this->assertTrue(
$GLOBALS['from_cookie']
);
$this->assertEquals(
'',
$this->object->password
);
}
public function testAuthCheckAuthFails(): void
{
$GLOBALS['server'] = 1;
$_REQUEST['old_usr'] = '';
$_POST['pma_username'] = '';
$_COOKIE['pmaServer-1'] = 'pmaServ1';
$_COOKIE['pmaUser-1'] = 'pmaUser1';
$_COOKIE['pma_iv-1'] = base64_encode('testiv09testiv09');
$GLOBALS['cfg']['blowfish_secret'] = 'secret';
$_SESSION['last_access_time'] = 1;
$GLOBALS['cfg']['CaptchaApi'] = '';
$GLOBALS['cfg']['CaptchaRequestParam'] = '';
$GLOBALS['cfg']['CaptchaResponseParam'] = '';
$GLOBALS['cfg']['CaptchaLoginPrivateKey'] = '';
$GLOBALS['cfg']['CaptchaLoginPublicKey'] = '';
$GLOBALS['cfg']['LoginCookieValidity'] = 0;
$_SESSION['browser_access_time']['default'] = -1;
$GLOBALS['PMA_Config']->set('is_https', false);
// mock for blowfish function
$this->object = $this->getMockBuilder(AuthenticationCookie::class)
->disableOriginalConstructor()
->setMethods(['showFailure', 'cookieDecrypt'])
->getMock();
$this->object->expects($this->once())
->method('cookieDecrypt')
->will($this->returnValue('testBF'));
$this->object->expects($this->once())
->method('showFailure');
$this->assertFalse(
$this->object->readCredentials()
);
}
public function testAuthSetUser(): void
{
$this->object->user = 'pmaUser2';
$arr = [
'host' => 'a',
'port' => 1,
'socket' => true,
'ssl' => true,
'user' => 'pmaUser2',
];
$GLOBALS['cfg']['Server'] = $arr;
$GLOBALS['cfg']['Server']['user'] = 'pmaUser';
$GLOBALS['cfg']['Servers'][1] = $arr;
$GLOBALS['cfg']['AllowArbitraryServer'] = true;
$GLOBALS['pma_auth_server'] = 'b 2';
$this->object->password = 'testPW';
$GLOBALS['server'] = 2;
$GLOBALS['cfg']['LoginCookieStore'] = true;
$GLOBALS['from_cookie'] = true;
$GLOBALS['PMA_Config']->set('is_https', false);
$this->object->storeCredentials();
$this->object->rememberCredentials();
$this->assertArrayHasKey(
'pmaUser-2',
$_COOKIE
);
$this->assertArrayHasKey(
'pmaAuth-2',
$_COOKIE
);
$arr['password'] = 'testPW';
$arr['host'] = 'b';
$arr['port'] = '2';
$this->assertEquals(
$arr,
$GLOBALS['cfg']['Server']
);
}
public function testAuthSetUserWithHeaders(): void
{
$this->object->user = 'pmaUser2';
$arr = [
'host' => 'a',
'port' => 1,
'socket' => true,
'ssl' => true,
'user' => 'pmaUser2',
];
$GLOBALS['cfg']['Server'] = $arr;
$GLOBALS['cfg']['Server']['host'] = 'b';
$GLOBALS['cfg']['Server']['user'] = 'pmaUser';
$GLOBALS['cfg']['Servers'][1] = $arr;
$GLOBALS['cfg']['AllowArbitraryServer'] = true;
$GLOBALS['pma_auth_server'] = 'b 2';
$this->object->password = 'testPW';
$GLOBALS['server'] = 2;
$GLOBALS['cfg']['LoginCookieStore'] = true;
$GLOBALS['from_cookie'] = false;
$this->mockResponse(
$this->stringContains('&server=2&lang=en')
);
$this->object->storeCredentials();
$this->object->rememberCredentials();
}
public function testAuthFailsNoPass(): void
{
$this->object = $this->getMockBuilder(AuthenticationCookie::class)
->disableOriginalConstructor()
->setMethods(['showLoginForm'])
->getMock();
$GLOBALS['server'] = 2;
$_COOKIE['pmaAuth-2'] = 'pass';
$this->mockResponse(
['Cache-Control: no-store, no-cache, must-revalidate'],
['Pragma: no-cache']
);
$this->object->showFailure('empty-denied');
$this->assertEquals(
$GLOBALS['conn_error'],
'Login without a password is forbidden by configuration'
. ' (see AllowNoPassword)'
);
}
public function dataProviderPasswordLength(): array
{
return [
[
str_repeat('a', 1000),
false,
'Your password is too long. To prevent denial-of-service attacks,'
. ' phpMyAdmin restricts passwords to less than 1000 characters.',
],
[
str_repeat('a', 1001),
false,
'Your password is too long. To prevent denial-of-service attacks,'
. ' phpMyAdmin restricts passwords to less than 1000 characters.',
],
[
str_repeat('a', 3000),
false,
'Your password is too long. To prevent denial-of-service attacks,'
. ' phpMyAdmin restricts passwords to less than 1000 characters.',
],
[
str_repeat('a', 256),
true,
null,
],
[
'',
true,
null,
],
];
}
/**
* @dataProvider dataProviderPasswordLength
*/
public function testAuthFailsTooLongPass(string $password, bool $trueFalse, ?string $connError): void
{
$_POST['pma_username'] = str_shuffle('123456987rootfoobar');
$_POST['pma_password'] = $password;
if ($trueFalse === false) {
$this->assertFalse(
$this->object->readCredentials()
);
} else {
$this->assertTrue(
$this->object->readCredentials()
);
}
$this->assertEquals(
$GLOBALS['conn_error'],
$connError
);
}
public function testAuthFailsDeny(): void
{
$this->object = $this->getMockBuilder(AuthenticationCookie::class)
->disableOriginalConstructor()
->setMethods(['showLoginForm'])
->getMock();
$GLOBALS['server'] = 2;
$_COOKIE['pmaAuth-2'] = 'pass';
$this->mockResponse(
['Cache-Control: no-store, no-cache, must-revalidate'],
['Pragma: no-cache']
);
$this->object->showFailure('allow-denied');
$this->assertEquals(
$GLOBALS['conn_error'],
'Access denied!'
);
}
public function testAuthFailsActivity(): void
{
$this->object = $this->getMockBuilder(AuthenticationCookie::class)
->disableOriginalConstructor()
->setMethods(['showLoginForm'])
->getMock();
$GLOBALS['server'] = 2;
$_COOKIE['pmaAuth-2'] = 'pass';
$GLOBALS['allowDeny_forbidden'] = '';
$GLOBALS['cfg']['LoginCookieValidity'] = 10;
$this->mockResponse(
['Cache-Control: no-store, no-cache, must-revalidate'],
['Pragma: no-cache']
);
$this->object->showFailure('no-activity');
$this->assertEquals(
$GLOBALS['conn_error'],
'You have been automatically logged out due to inactivity of 10 seconds.'
. ' Once you log in again, you should be able to resume the work where you left off.'
);
}
public function testAuthFailsDBI(): void
{
$this->object = $this->getMockBuilder(AuthenticationCookie::class)
->disableOriginalConstructor()
->setMethods(['showLoginForm'])
->getMock();
$GLOBALS['server'] = 2;
$_COOKIE['pmaAuth-2'] = 'pass';
$dbi = $this->getMockBuilder(DatabaseInterface::class)
->disableOriginalConstructor()
->getMock();
$dbi->expects($this->at(0))
->method('getError')
->will($this->returnValue(false));
$GLOBALS['dbi'] = $dbi;
$GLOBALS['errno'] = 42;
$this->mockResponse(
['Cache-Control: no-store, no-cache, must-revalidate'],
['Pragma: no-cache']
);
$this->object->showFailure('');
$this->assertEquals(
$GLOBALS['conn_error'],
'#42 Cannot log in to the MySQL server'
);
}
public function testAuthFailsErrno(): void
{
$this->object = $this->getMockBuilder(AuthenticationCookie::class)
->disableOriginalConstructor()
->setMethods(['showLoginForm'])
->getMock();
$dbi = $this->getMockBuilder(DatabaseInterface::class)
->disableOriginalConstructor()
->getMock();
$dbi->expects($this->at(0))
->method('getError')
->will($this->returnValue(false));
$GLOBALS['dbi'] = $dbi;
$GLOBALS['server'] = 2;
$_COOKIE['pmaAuth-2'] = 'pass';
unset($GLOBALS['errno']);
$this->mockResponse(
['Cache-Control: no-store, no-cache, must-revalidate'],
['Pragma: no-cache']
);
$this->object->showFailure('');
$this->assertEquals(
$GLOBALS['conn_error'],
'Cannot log in to the MySQL server'
);
}
public function testGetEncryptionSecretEmpty(): void
{
$method = new ReflectionMethod(
AuthenticationCookie::class,
'getEncryptionSecret'
);
$method->setAccessible(true);
$GLOBALS['cfg']['blowfish_secret'] = '';
$_SESSION['encryption_key'] = '';
$result = $method->invoke($this->object, null);
$this->assertEquals(
$result,
$_SESSION['encryption_key']
);
$this->assertEquals(
32,
strlen($result)
);
}
public function testGetEncryptionSecretConfigured(): void
{
$method = new ReflectionMethod(
AuthenticationCookie::class,
'getEncryptionSecret'
);
$method->setAccessible(true);
$GLOBALS['cfg']['blowfish_secret'] = 'notEmpty';
$result = $method->invoke($this->object, null);
$this->assertEquals(
'notEmpty',
$result
);
}
public function testCookieEncrypt(): void
{
$this->object->setIV('testiv09testiv09');
// works with the openssl extension active or inactive
$this->assertEquals(
'{"iv":"dGVzdGl2MDl0ZXN0aXYwOQ==","mac":"347aa45ae1ade00c980f31129ec2def'
. 'ef18b2bfd","payload":"YDEaxOfP9nD9q\/2pC6hjfQ=="}',
$this->object->cookieEncrypt('data123', 'sec321')
);
}
public function testCookieEncryptPHPSecLib(): void
{
$this->object->setUseOpenSSL(false);
$this->testCookieEncrypt();
}
public function testCookieEncryptOpenSSL(): void
{
if (! function_exists('openssl_encrypt')) {
$this->markTestSkipped('openssl not available');
}
$this->object->setUseOpenSSL(true);
$this->testCookieEncrypt();
}
public function testCookieDecrypt(): void
{
// works with the openssl extension active or inactive
$this->assertEquals(
'data123',
$this->object->cookieDecrypt(
'{"iv":"dGVzdGl2MDl0ZXN0aXYwOQ==","mac":"347aa45ae1ade00c980f31129ec'
. '2defef18b2bfd","payload":"YDEaxOfP9nD9q\/2pC6hjfQ=="}',
'sec321'
)
);
$this->assertEquals(
'root',
$this->object->cookieDecrypt(
'{"iv":"AclJhCM7ryNiuPnw3Y8cXg==","mac":"d0ef75e852bc162e81496e116dc'
. '571182cb2cba6","payload":"O4vrt9R1xyzAw7ypvrLmQA=="}',
':Kb1?)c(r{]-{`HW*hOzuufloK(M~!p'
)
);
$this->assertFalse(
$this->object->cookieDecrypt(
'{"iv":"AclJhCM7ryNiuPnw3Y8cXg==","mac":"d0ef75e852bc162e81496e116dc'
. '571182cb2cba6","payload":"O4vrt9R1xyzAw7ypvrLmQA=="}',
'aedzoiefpzf,zf1z7ef6ef84'
)
);
}
public function testCookieDecryptPHPSecLib(): void
{
$this->object->setUseOpenSSL(false);
$this->testCookieDecrypt();
}
public function testCookieDecryptOpenSSL(): void
{
if (! function_exists('openssl_encrypt')) {
$this->markTestSkipped('openssl not available');
}
$this->object->setUseOpenSSL(true);
$this->testCookieDecrypt();
}
public function testCookieDecryptInvalid(): void
{
// works with the openssl extension active or inactive
$this->assertFalse(
$this->object->cookieDecrypt(
'{"iv":0,"mac":0,"payload":0}',
'sec321'
)
);
}
/**
* Test for secret splitting using getAESSecret
*
* @param string $secret secret
* @param string $mac mac
* @param string $aes aes
*
* @dataProvider secretsProvider
*/
public function testMACSecretSplit(string $secret, string $mac, string $aes): void
{
$this->assertNotEmpty($aes);// Useless check
$this->assertEquals(
$mac,
$this->object->getMACSecret($secret)
);
}
/**
* Test for secret splitting using getMACSecret and getAESSecret
*
* @param string $secret secret
* @param string $mac mac
* @param string $aes aes
*
* @dataProvider secretsProvider
*/
public function testAESSecretSplit(string $secret, string $mac, string $aes): void
{
$this->assertNotEmpty($mac);// Useless check
$this->assertEquals(
$aes,
$this->object->getAESSecret($secret)
);
}
/**
* @throws ReflectionException
*/
public function testPasswordChange(): void
{
$newPassword = 'PMAPASSWD2';
$GLOBALS['PMA_Config']->set('is_https', false);
$GLOBALS['cfg']['AllowArbitraryServer'] = true;
$GLOBALS['pma_auth_server'] = 'b 2';
$_SESSION['encryption_key'] = '';
$this->object->setIV('testiv09testiv09');
$this->object->handlePasswordChange($newPassword);
$payload = [
'password' => $newPassword,
'server' => 'b 2',
];
$method = new ReflectionMethod(
AuthenticationCookie::class,
'getSessionEncryptionSecret'
);
$method->setAccessible(true);
$encryptedCookie = $this->object->cookieEncrypt(
(string) json_encode($payload),
$method->invoke($this->object, null)
);
$this->assertEquals(
$_COOKIE['pmaAuth-' . $GLOBALS['server']],
$encryptedCookie
);
}
/**
* Data provider for secrets splitting.
*
* @return array
*/
public function secretsProvider(): array
{
return [
// Optimal case
[
'1234567890123456abcdefghijklmnop',
'1234567890123456',
'abcdefghijklmnop',
],
// Overlapping secret
[
'12345678901234567',
'1234567890123456',
'2345678901234567',
],
// Short secret
[
'1234567890123456',
'1234567890123451',
'2345678901234562',
],
// Really short secret
[
'12',
'1111111111111111',
'2222222222222222',
],
// Too short secret
[
'1',
'1111111111111111',
'1111111111111111',
],
];
}
public function testAuthenticate(): void
{
$GLOBALS['cfg']['CaptchaApi'] = '';
$GLOBALS['cfg']['CaptchaRequestParam'] = '';
$GLOBALS['cfg']['CaptchaResponseParam'] = '';
$GLOBALS['cfg']['CaptchaLoginPrivateKey'] = '';
$GLOBALS['cfg']['CaptchaLoginPublicKey'] = '';
$GLOBALS['cfg']['Server']['AllowRoot'] = false;
$GLOBALS['cfg']['Server']['AllowNoPassword'] = false;
$_REQUEST['old_usr'] = '';
$_POST['pma_username'] = 'testUser';
$_POST['pma_password'] = 'testPassword';
ob_start();
$this->object->authenticate();
$result = ob_get_clean();
/* Nothing should be printed */
$this->assertEquals('', $result);
/* Verify readCredentials worked */
$this->assertEquals('testUser', $this->object->user);
$this->assertEquals('testPassword', $this->object->password);
/* Verify storeCredentials worked */
$this->assertEquals('testUser', $GLOBALS['cfg']['Server']['user']);
$this->assertEquals('testPassword', $GLOBALS['cfg']['Server']['password']);
}
/**
* @param string $user user
* @param string $pass pass
* @param string $ip ip
* @param bool $root root
* @param bool $nopass nopass
* @param array $rules rules
* @param string $expected expected result
*
* @dataProvider checkRulesProvider
*/
public function testCheckRules(
string $user,
string $pass,
string $ip,
bool $root,
bool $nopass,
array $rules,
string $expected
): void {
$this->object->user = $user;
$this->object->password = $pass;
$this->object->storeCredentials();
$_SERVER['REMOTE_ADDR'] = $ip;
$GLOBALS['cfg']['Server']['AllowRoot'] = $root;
$GLOBALS['cfg']['Server']['AllowNoPassword'] = $nopass;
$GLOBALS['cfg']['Server']['AllowDeny'] = $rules;
if (! empty($expected)) {
$this->getAuthErrorMockResponse();
}
ob_start();
$this->object->checkRules();
$result = ob_get_clean();
$this->assertIsString($result);
if (empty($expected)) {
$this->assertEquals($expected, $result);
} else {
$this->assertStringContainsString($expected, $result);
}
}
public function checkRulesProvider(): array
{
return [
'nopass-ok' => [
'testUser',
'',
'1.2.3.4',
true,
true,
[],
'',
],
'nopass' => [
'testUser',
'',
'1.2.3.4',
true,
false,
[],
'Login without a password is forbidden',
],
'root-ok' => [
'root',
'root',
'1.2.3.4',
true,
true,
[],
'',
],
'root' => [
'root',
'root',
'1.2.3.4',
false,
true,
[],
'Access denied!',
],
'rules-deny-allow-ok' => [
'root',
'root',
'1.2.3.4',
true,
true,
[
'order' => 'deny,allow',
'rules' => [
'allow root 1.2.3.4',
'deny % from all',
],
],
'',
],
'rules-deny-allow-reject' => [
'user',
'root',
'1.2.3.4',
true,
true,
[
'order' => 'deny,allow',
'rules' => [
'allow root 1.2.3.4',
'deny % from all',
],
],
'Access denied!',
],
'rules-allow-deny-ok' => [
'root',
'root',
'1.2.3.4',
true,
true,
[
'order' => 'allow,deny',
'rules' => [
'deny user from all',
'allow root 1.2.3.4',
],
],
'',
],
'rules-allow-deny-reject' => [
'user',
'root',
'1.2.3.4',
true,
true,
[
'order' => 'allow,deny',
'rules' => [
'deny user from all',
'allow root 1.2.3.4',
],
],
'Access denied!',
],
'rules-explicit-ok' => [
'root',
'root',
'1.2.3.4',
true,
true,
[
'order' => 'explicit',
'rules' => [
'deny user from all',
'allow root 1.2.3.4',
],
],
'',
],
'rules-explicit-reject' => [
'user',
'root',
'1.2.3.4',
true,
true,
[
'order' => 'explicit',
'rules' => [
'deny user from all',
'allow root 1.2.3.4',
],
],
'Access denied!',
],
];
}
}