diff options
Diffstat (limited to 'srcs/phpmyadmin/libraries/classes/Plugins/Auth')
4 files changed, 1632 insertions, 0 deletions
diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationConfig.php b/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationConfig.php new file mode 100644 index 0000000..7ebd1ae --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationConfig.php @@ -0,0 +1,172 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Config Authentication plugin for phpMyAdmin + * + * @package PhpMyAdmin-Authentication + * @subpackage Config + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Plugins\Auth; + +use PhpMyAdmin\Plugins\AuthenticationPlugin; +use PhpMyAdmin\Response; +use PhpMyAdmin\Server\Select; +use PhpMyAdmin\Url; +use PhpMyAdmin\Util; + +/** + * Handles the config authentication method + * + * @package PhpMyAdmin-Authentication + */ +class AuthenticationConfig extends AuthenticationPlugin +{ + /** + * Displays authentication form + * + * @return boolean always true + */ + public function showLoginForm() + { + $response = Response::getInstance(); + if ($response->isAjax()) { + $response->setRequestStatus(false); + // reload_flag removes the token parameter from the URL and reloads + $response->addJSON('reload_flag', '1'); + if (defined('TESTSUITE')) { + return true; + } else { + exit; + } + } + + return true; + } + + /** + * Gets authentication credentials + * + * @return boolean always true + */ + public function readCredentials() + { + if ($GLOBALS['token_provided'] && $GLOBALS['token_mismatch']) { + return false; + } + + $this->user = $GLOBALS['cfg']['Server']['user']; + $this->password = $GLOBALS['cfg']['Server']['password']; + + return true; + } + + /** + * User is not allowed to login to MySQL -> authentication failed + * + * @param string $failure String describing why authentication has failed + * + * @return void + */ + public function showFailure($failure) + { + parent::showFailure($failure); + $conn_error = $GLOBALS['dbi']->getError(); + if (! $conn_error) { + $conn_error = __('Cannot connect: invalid settings.'); + } + + /* HTML header */ + $response = Response::getInstance(); + $response->getFooter() + ->setMinimal(); + $header = $response->getHeader(); + $header->setBodyId('loginform'); + $header->setTitle(__('Access denied!')); + $header->disableMenuAndConsole(); + echo '<br><br> + <center> + <h1>'; + echo sprintf(__('Welcome to %s'), ' phpMyAdmin '); + echo '</h1> + </center> + <br> + <table cellpadding="0" cellspacing="3" class= "auth_config_tbl" width="80%"> + <tr> + <td>'; + if (isset($GLOBALS['allowDeny_forbidden']) + && $GLOBALS['allowDeny_forbidden'] + ) { + trigger_error(__('Access denied!'), E_USER_NOTICE); + } else { + // Check whether user has configured something + if ($GLOBALS['PMA_Config']->source_mtime == 0) { + echo '<p>' , sprintf( + __( + 'You probably did not create a configuration file.' + . ' You might want to use the %1$ssetup script%2$s to' + . ' create one.' + ), + '<a href="setup/">', + '</a>' + ) , '</p>' , "\n"; + } elseif (! isset($GLOBALS['errno']) + || (isset($GLOBALS['errno']) && $GLOBALS['errno'] != 2002) + && $GLOBALS['errno'] != 2003 + ) { + // if we display the "Server not responding" error, do not confuse + // users by telling them they have a settings problem + // (note: it's true that they could have a badly typed host name, + // but anyway the current message tells that the server + // rejected the connection, which is not really what happened) + // 2002 is the error given by mysqli + // 2003 is the error given by mysql + trigger_error( + __( + 'phpMyAdmin tried to connect to the MySQL server, and the' + . ' server rejected the connection. You should check the' + . ' host, username and password in your configuration and' + . ' make sure that they correspond to the information given' + . ' by the administrator of the MySQL server.' + ), + E_USER_WARNING + ); + } + echo Util::mysqlDie( + $conn_error, + '', + true, + '', + false + ); + } + $GLOBALS['error_handler']->dispUserErrors(); + echo '</td> + </tr> + <tr> + <td>' , "\n"; + echo '<a href="' + , Util::getScriptNameForOption( + $GLOBALS['cfg']['DefaultTabServer'], + 'server' + ) + , Url::getCommon() , '" class="button disableAjax">' + , __('Retry to connect') + , '</a>' , "\n"; + echo '</td> + </tr>' , "\n"; + if (count($GLOBALS['cfg']['Servers']) > 1) { + // offer a chance to login to other servers if the current one failed + echo '<tr>' , "\n"; + echo ' <td>' , "\n"; + echo Select::render(true, true); + echo ' </td>' , "\n"; + echo '</tr>' , "\n"; + } + echo '</table>' , "\n"; + if (! defined('TESTSUITE')) { + exit; + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationCookie.php b/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationCookie.php new file mode 100644 index 0000000..7a794d0 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationCookie.php @@ -0,0 +1,964 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Cookie Authentication plugin for phpMyAdmin + * + * @package PhpMyAdmin-Authentication + * @subpackage Cookie + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Plugins\Auth; + +use PhpMyAdmin\Config; +use PhpMyAdmin\Core; +use PhpMyAdmin\LanguageManager; +use PhpMyAdmin\Message; +use PhpMyAdmin\Plugins\AuthenticationPlugin; +use PhpMyAdmin\Response; +use PhpMyAdmin\Server\Select; +use PhpMyAdmin\Session; +use PhpMyAdmin\Template; +use PhpMyAdmin\Url; +use PhpMyAdmin\Util; +use phpseclib\Crypt; +use phpseclib\Crypt\Random; +use ReCaptcha; + +/** + * Remember where to redirect the user + * in case of an expired session. + */ +if (! empty($_REQUEST['target'])) { + $GLOBALS['target'] = $_REQUEST['target']; +} elseif (Core::getenv('SCRIPT_NAME')) { + $GLOBALS['target'] = basename(Core::getenv('SCRIPT_NAME')); +} + +/** + * Handles the cookie authentication method + * + * @package PhpMyAdmin-Authentication + */ +class AuthenticationCookie extends AuthenticationPlugin +{ + /** + * IV for encryption + */ + private $_cookie_iv = null; + + /** + * Whether to use OpenSSL directly + */ + private $_use_openssl; + + /** + * Constructor + */ + public function __construct() + { + parent::__construct(); + $this->_use_openssl = ! class_exists(Random::class); + } + + /** + * Forces (not)using of openSSL + * + * @param boolean $use The flag + * + * @return void + */ + public function setUseOpenSSL($use) + { + $this->_use_openssl = $use; + } + + /** + * Displays authentication form + * + * this function MUST exit/quit the application + * + * @global string $conn_error the last connection error + * + * @return boolean|void + */ + public function showLoginForm() + { + global $conn_error; + + $response = Response::getInstance(); + + // When sending login modal after session has expired, send the new token explicitly with the response to update the token in all the forms having a hidden token. + $session_expired = isset($_REQUEST['check_timeout']) || isset($_REQUEST['session_timedout']); + if (! $session_expired && $response->loginPage()) { + if (defined('TESTSUITE')) { + return true; + } else { + exit; + } + } + + // When sending login modal after session has expired, send the new token explicitly with the response to update the token in all the forms having a hidden token. + if ($session_expired) { + $response->setRequestStatus(false); + $response->addJSON( + 'new_token', + $_SESSION[' PMA_token '] + ); + } + + // logged_in response parameter is used to check if the login, using the modal was successful after session expiration + if (isset($_REQUEST['session_timedout'])) { + $response->addJSON( + 'logged_in', + 0 + ); + } + + // No recall if blowfish secret is not configured as it would produce + // garbage + if ($GLOBALS['cfg']['LoginCookieRecall'] + && ! empty($GLOBALS['cfg']['blowfish_secret']) + ) { + $default_user = $this->user; + $default_server = $GLOBALS['pma_auth_server']; + $autocomplete = ''; + } else { + $default_user = ''; + $default_server = ''; + // skip the IE autocomplete feature. + $autocomplete = ' autocomplete="off"'; + } + + // wrap the login form in a div which overlays the whole page. + if ($session_expired) { + echo $this->template->render('login/header', [ + 'theme' => $GLOBALS['PMA_Theme'], + 'add_class' => ' modal_form', + 'session_expired' => 1, + ]); + } else { + echo $this->template->render('login/header', [ + 'theme' => $GLOBALS['PMA_Theme'], + 'add_class' => '', + 'session_expired' => 0, + ]); + } + + if ($GLOBALS['cfg']['DBG']['demo']) { + echo '<fieldset>'; + echo '<legend>' , __('phpMyAdmin Demo Server') , '</legend>'; + printf( + __( + 'You are using the demo server. You can do anything here, but ' + . 'please do not change root, debian-sys-maint and pma users. ' + . 'More information is available at %s.' + ), + '<a href="url.php?url=https://demo.phpmyadmin.net/" target="_blank" rel="noopener noreferrer">demo.phpmyadmin.net</a>' + ); + echo '</fieldset>'; + } + + // Show error message + if (! empty($conn_error)) { + Message::rawError((string) $conn_error)->display(); + } elseif (isset($_GET['session_expired']) + && intval($_GET['session_expired']) == 1 + ) { + Message::rawError( + __('Your session has expired. Please log in again.') + )->display(); + } + + // Displays the languages form + $language_manager = LanguageManager::getInstance(); + if (empty($GLOBALS['cfg']['Lang']) && $language_manager->hasChoice()) { + echo "<div class='hide js-show'>"; + // use fieldset, don't show doc link + echo $language_manager->getSelectorDisplay(new Template(), true, false); + echo '</div>'; + } + echo ' + <br> + <!-- Login form --> + <form method="post" id="login_form" action="index.php" name="login_form"' , $autocomplete , + ' class="' . ($session_expired ? "" : "disableAjax hide ") . 'login js-show"> + <fieldset> + <legend>'; + echo '<input type="hidden" name="set_session" value="', htmlspecialchars(session_id()), '">'; + + // Add a hidden element session_timedout which is used to check if the user requested login after session expiration + if ($session_expired) { + echo '<input type="hidden" name="session_timedout" value="1">'; + } + echo __('Log in'); + echo Util::showDocu('index'); + echo '</legend>'; + if ($GLOBALS['cfg']['AllowArbitraryServer']) { + echo ' + <div class="item"> + <label for="input_servername" title="'; + echo __( + 'You can enter hostname/IP address and port separated by space.' + ); + echo '">'; + echo __('Server:'); + echo '</label> + <input type="text" name="pma_servername" id="input_servername"'; + echo ' value="'; + echo htmlspecialchars($default_server); + echo '" size="24" class="textfield" title="'; + echo __( + 'You can enter hostname/IP address and port separated by space.' + ); echo '"> + </div>'; + } + echo '<div class="item"> + <label for="input_username">' , __('Username:') , '</label> + <input type="text" name="pma_username" id="input_username" ' + , 'value="' , htmlspecialchars($default_user) , '" size="24"' + , ' class="textfield"> + </div> + <div class="item"> + <label for="input_password">' , __('Password:') , '</label> + <input type="password" name="pma_password" id="input_password"' + , ' value="" size="24" class="textfield"> + </div>'; + if (count($GLOBALS['cfg']['Servers']) > 1) { + echo '<div class="item"> + <label for="select_server">' . __('Server Choice:') . '</label> + <select name="server" id="select_server"'; + if ($GLOBALS['cfg']['AllowArbitraryServer']) { + echo ' onchange="document.forms[\'login_form\'].' + , 'elements[\'pma_servername\'].value = \'\'" '; + } + echo '>'; + echo Select::render(false, false); + echo '</select></div>'; + } else { + echo ' <input type="hidden" name="server" value="' + , $GLOBALS['server'] , '">'; + } // end if (server choice) + + echo '</fieldset><fieldset class="tblFooters">'; + + // binds input field with invisible reCaptcha if enabled + if (empty($GLOBALS['cfg']['CaptchaLoginPrivateKey']) + && empty($GLOBALS['cfg']['CaptchaLoginPublicKey']) + ) { + echo '<input class="btn btn-primary" value="' , __('Go') , '" type="submit" id="input_go">'; + } else { + echo '<script src="https://www.google.com/recaptcha/api.js?hl=' + , $GLOBALS['lang'] , '" async defer></script>'; + echo '<input class="btn btn-primary g-recaptcha" data-sitekey="' + , htmlspecialchars($GLOBALS['cfg']['CaptchaLoginPublicKey']),'"' + . ' data-callback="Functions_recaptchaCallback" value="' , __('Go') , '" type="submit" id="input_go">'; + } + $_form_params = []; + if (! empty($GLOBALS['target'])) { + $_form_params['target'] = $GLOBALS['target']; + } + if (strlen($GLOBALS['db'])) { + $_form_params['db'] = $GLOBALS['db']; + } + if (strlen($GLOBALS['table'])) { + $_form_params['table'] = $GLOBALS['table']; + } + // do not generate a "server" hidden field as we want the "server" + // drop-down to have priority + echo Url::getHiddenInputs($_form_params, '', 0, 'server'); + echo '</fieldset> + </form>'; + + if ($GLOBALS['error_handler']->hasDisplayErrors()) { + echo '<div id="pma_errors">'; + $GLOBALS['error_handler']->dispErrors(); + echo '</div>'; + } + + // close the wrapping div tag, if the request is after session timeout + if ($session_expired) { + echo $this->template->render('login/footer', ['session_expired' => 1]); + } else { + echo $this->template->render('login/footer', ['session_expired' => 0]); + } + + echo Config::renderFooter(); + + if (! defined('TESTSUITE')) { + exit; + } else { + return true; + } + } + + /** + * Gets authentication credentials + * + * this function DOES NOT check authentication - it just checks/provides + * authentication credentials required to connect to the MySQL server + * usually with $GLOBALS['dbi']->connect() + * + * it returns false if something is missing - which usually leads to + * showLoginForm() which displays login form + * + * it returns true if all seems ok which usually leads to auth_set_user() + * + * it directly switches to showFailure() if user inactivity timeout is reached + * + * @return boolean whether we get authentication settings or not + */ + public function readCredentials() + { + global $conn_error; + + // Initialization + /** + * @global $GLOBALS['pma_auth_server'] the user provided server to + * connect to + */ + $GLOBALS['pma_auth_server'] = ''; + + $this->user = $this->password = ''; + $GLOBALS['from_cookie'] = false; + + if (isset($_POST['pma_username']) && strlen($_POST['pma_username']) > 0) { + // Verify Captcha if it is required. + if (! empty($GLOBALS['cfg']['CaptchaLoginPrivateKey']) + && ! empty($GLOBALS['cfg']['CaptchaLoginPublicKey']) + ) { + if (! empty($_POST["g-recaptcha-response"])) { + if (function_exists('curl_init')) { + $reCaptcha = new ReCaptcha\ReCaptcha( + $GLOBALS['cfg']['CaptchaLoginPrivateKey'], + new ReCaptcha\RequestMethod\CurlPost() + ); + } elseif (ini_get('allow_url_fopen')) { + $reCaptcha = new ReCaptcha\ReCaptcha( + $GLOBALS['cfg']['CaptchaLoginPrivateKey'], + new ReCaptcha\RequestMethod\Post() + ); + } else { + $reCaptcha = new ReCaptcha\ReCaptcha( + $GLOBALS['cfg']['CaptchaLoginPrivateKey'], + new ReCaptcha\RequestMethod\SocketPost() + ); + } + + // verify captcha status. + $resp = $reCaptcha->verify( + $_POST["g-recaptcha-response"], + Core::getIp() + ); + + // Check if the captcha entered is valid, if not stop the login. + if ($resp == null || ! $resp->isSuccess()) { + $codes = $resp->getErrorCodes(); + + if (in_array('invalid-json', $codes)) { + $conn_error = __('Failed to connect to the reCAPTCHA service!'); + } else { + $conn_error = __('Entered captcha is wrong, try again!'); + } + return false; + } + } else { + $conn_error = __('Missing reCAPTCHA verification, maybe it has been blocked by adblock?'); + return false; + } + } + + // The user just logged in + $this->user = Core::sanitizeMySQLUser($_POST['pma_username']); + $this->password = isset($_POST['pma_password']) ? $_POST['pma_password'] : ''; + if ($GLOBALS['cfg']['AllowArbitraryServer'] + && isset($_REQUEST['pma_servername']) + ) { + if ($GLOBALS['cfg']['ArbitraryServerRegexp']) { + $parts = explode(' ', $_REQUEST['pma_servername']); + if (count($parts) === 2) { + $tmp_host = $parts[0]; + } else { + $tmp_host = $_REQUEST['pma_servername']; + } + + $match = preg_match( + $GLOBALS['cfg']['ArbitraryServerRegexp'], + $tmp_host + ); + if (! $match) { + $conn_error = __( + 'You are not allowed to log in to this MySQL server!' + ); + return false; + } + } + $GLOBALS['pma_auth_server'] = Core::sanitizeMySQLHost($_REQUEST['pma_servername']); + } + /* Secure current session on login to avoid session fixation */ + Session::secure(); + return true; + } + + // At the end, try to set the $this->user + // and $this->password variables from cookies + + // check cookies + $serverCookie = $GLOBALS['PMA_Config']->getCookie('pmaUser-' . $GLOBALS['server']); + if (empty($serverCookie)) { + return false; + } + + $value = $this->cookieDecrypt( + $serverCookie, + $this->_getEncryptionSecret() + ); + + if ($value === false) { + return false; + } + + $this->user = $value; + // user was never logged in since session start + if (empty($_SESSION['browser_access_time'])) { + return false; + } + + // User inactive too long + $last_access_time = time() - $GLOBALS['cfg']['LoginCookieValidity']; + foreach ($_SESSION['browser_access_time'] as $key => $value) { + if ($value < $last_access_time) { + unset($_SESSION['browser_access_time'][$key]); + } + } + // All sessions expired + if (empty($_SESSION['browser_access_time'])) { + Util::cacheUnset('is_create_db_priv'); + Util::cacheUnset('is_reload_priv'); + Util::cacheUnset('db_to_create'); + Util::cacheUnset('dbs_where_create_table_allowed'); + Util::cacheUnset('dbs_to_test'); + Util::cacheUnset('db_priv'); + Util::cacheUnset('col_priv'); + Util::cacheUnset('table_priv'); + Util::cacheUnset('proc_priv'); + + $this->showFailure('no-activity'); + if (! defined('TESTSUITE')) { + exit; + } else { + return false; + } + } + + // check password cookie + $serverCookie = $GLOBALS['PMA_Config']->getCookie('pmaAuth-' . $GLOBALS['server']); + + if (empty($serverCookie)) { + return false; + } + $value = $this->cookieDecrypt( + $serverCookie, + $this->_getSessionEncryptionSecret() + ); + if ($value === false) { + return false; + } + + $auth_data = json_decode($value, true); + + if (! is_array($auth_data) || ! isset($auth_data['password'])) { + return false; + } + $this->password = $auth_data['password']; + if ($GLOBALS['cfg']['AllowArbitraryServer'] && ! empty($auth_data['server'])) { + $GLOBALS['pma_auth_server'] = $auth_data['server']; + } + + $GLOBALS['from_cookie'] = true; + + return true; + } + + /** + * Set the user and password after last checkings if required + * + * @return boolean always true + */ + public function storeCredentials() + { + global $cfg; + + if ($GLOBALS['cfg']['AllowArbitraryServer'] + && ! empty($GLOBALS['pma_auth_server']) + ) { + /* Allow to specify 'host port' */ + $parts = explode(' ', $GLOBALS['pma_auth_server']); + if (count($parts) === 2) { + $tmp_host = $parts[0]; + $tmp_port = $parts[1]; + } else { + $tmp_host = $GLOBALS['pma_auth_server']; + $tmp_port = ''; + } + if ($cfg['Server']['host'] != $GLOBALS['pma_auth_server']) { + $cfg['Server']['host'] = $tmp_host; + if (! empty($tmp_port)) { + $cfg['Server']['port'] = $tmp_port; + } + } + unset($tmp_host, $tmp_port, $parts); + } + + return parent::storeCredentials(); + } + + /** + * Stores user credentials after successful login. + * + * @return void|bool + */ + public function rememberCredentials() + { + // Name and password cookies need to be refreshed each time + // Duration = one month for username + + $this->storeUsernameCookie($this->user); + + // Duration = as configured + // Do not store password cookie on password change as we will + // set the cookie again after password has been changed + if (! isset($_POST['change_pw'])) { + $this->storePasswordCookie($this->password); + } + // URL where to go: + $redirect_url = './index.php'; + + // any parameters to pass? + $url_params = []; + if (strlen($GLOBALS['db']) > 0) { + $url_params['db'] = $GLOBALS['db']; + } + if (strlen($GLOBALS['table']) > 0) { + $url_params['table'] = $GLOBALS['table']; + } + // any target to pass? + if (! empty($GLOBALS['target']) + && $GLOBALS['target'] != 'index.php' + ) { + $url_params['target'] = $GLOBALS['target']; + } + + // user logged in successfully after session expiration + if (isset($_REQUEST['session_timedout'])) { + $response = Response::getInstance(); + $response->addJSON( + 'logged_in', + 1 + ); + $response->addJSON( + 'success', + 1 + ); + $response->addJSON( + 'new_token', + $_SESSION[' PMA_token '] + ); + + if (! defined('TESTSUITE')) { + exit; + } else { + return false; + } + } + // Set server cookies if required (once per session) and, in this case, + // force reload to ensure the client accepts cookies + if (! $GLOBALS['from_cookie']) { + + /** + * Clear user cache. + */ + Util::clearUserCache(); + + Response::getInstance() + ->disable(); + + Core::sendHeaderLocation( + $redirect_url . Url::getCommonRaw($url_params), + true + ); + if (! defined('TESTSUITE')) { + exit; + } else { + return false; + } + } // end if + + return true; + } + + /** + * Stores username in a cookie. + * + * @param string $username User name + * + * @return void + */ + public function storeUsernameCookie($username) + { + // Name and password cookies need to be refreshed each time + // Duration = one month for username + $GLOBALS['PMA_Config']->setCookie( + 'pmaUser-' . $GLOBALS['server'], + $this->cookieEncrypt( + $username, + $this->_getEncryptionSecret() + ) + ); + } + + /** + * Stores password in a cookie. + * + * @param string $password Password + * + * @return void + */ + public function storePasswordCookie($password) + { + $payload = ['password' => $password]; + if ($GLOBALS['cfg']['AllowArbitraryServer'] && ! empty($GLOBALS['pma_auth_server'])) { + $payload['server'] = $GLOBALS['pma_auth_server']; + } + // Duration = as configured + $GLOBALS['PMA_Config']->setCookie( + 'pmaAuth-' . $GLOBALS['server'], + $this->cookieEncrypt( + json_encode($payload), + $this->_getSessionEncryptionSecret() + ), + null, + (int) $GLOBALS['cfg']['LoginCookieStore'] + ); + } + + /** + * User is not allowed to login to MySQL -> authentication failed + * + * prepares error message and switches to showLoginForm() which display the error + * and the login form + * + * this function MUST exit/quit the application, + * currently done by call to showLoginForm() + * + * @param string $failure String describing why authentication has failed + * + * @return void + */ + public function showFailure($failure) + { + global $conn_error; + + parent::showFailure($failure); + + // Deletes password cookie and displays the login form + $GLOBALS['PMA_Config']->removeCookie('pmaAuth-' . $GLOBALS['server']); + + $conn_error = $this->getErrorMessage($failure); + + $response = Response::getInstance(); + + // needed for PHP-CGI (not need for FastCGI or mod-php) + $response->header('Cache-Control: no-store, no-cache, must-revalidate'); + $response->header('Pragma: no-cache'); + + $this->showLoginForm(); + } + + /** + * Returns blowfish secret or generates one if needed. + * + * @return string + */ + private function _getEncryptionSecret() + { + if (empty($GLOBALS['cfg']['blowfish_secret'])) { + return $this->_getSessionEncryptionSecret(); + } + + return $GLOBALS['cfg']['blowfish_secret']; + } + + /** + * Returns blowfish secret or generates one if needed. + * + * @return string + */ + private function _getSessionEncryptionSecret() + { + if (empty($_SESSION['encryption_key'])) { + if ($this->_use_openssl) { + $_SESSION['encryption_key'] = openssl_random_pseudo_bytes(32); + } else { + $_SESSION['encryption_key'] = Crypt\Random::string(32); + } + } + return $_SESSION['encryption_key']; + } + + /** + * Concatenates secret in order to make it 16 bytes log + * + * This doesn't add any security, just ensures the secret + * is long enough by copying it. + * + * @param string $secret Original secret + * + * @return string + */ + public function enlargeSecret($secret) + { + while (strlen($secret) < 16) { + $secret .= $secret; + } + return substr($secret, 0, 16); + } + + /** + * Derives MAC secret from encryption secret. + * + * @param string $secret the secret + * + * @return string the MAC secret + */ + public function getMACSecret($secret) + { + // Grab first part, up to 16 chars + // The MAC and AES secrets can overlap if original secret is short + $length = strlen($secret); + if ($length > 16) { + return substr($secret, 0, 16); + } + return $this->enlargeSecret( + $length == 1 ? $secret : substr($secret, 0, -1) + ); + } + + /** + * Derives AES secret from encryption secret. + * + * @param string $secret the secret + * + * @return string the AES secret + */ + public function getAESSecret($secret) + { + // Grab second part, up to 16 chars + // The MAC and AES secrets can overlap if original secret is short + $length = strlen($secret); + if ($length > 16) { + return substr($secret, -16); + } + return $this->enlargeSecret( + $length == 1 ? $secret : substr($secret, 1) + ); + } + + /** + * Cleans any SSL errors + * + * This can happen from corrupted cookies, by invalid encryption + * parameters used in older phpMyAdmin versions or by wrong openSSL + * configuration. + * + * In neither case the error is useful to user, but we need to clear + * the error buffer as otherwise the errors would pop up later, for + * example during MySQL SSL setup. + * + * @return void + */ + public function cleanSSLErrors() + { + if (function_exists('openssl_error_string')) { + do { + $hasSslErrors = openssl_error_string(); + } while ($hasSslErrors !== false); + } + } + + /** + * Encryption using openssl's AES or phpseclib's AES + * (phpseclib uses mcrypt when it is available) + * + * @param string $data original data + * @param string $secret the secret + * + * @return string the encrypted result + */ + public function cookieEncrypt($data, $secret) + { + $mac_secret = $this->getMACSecret($secret); + $aes_secret = $this->getAESSecret($secret); + $iv = $this->createIV(); + if ($this->_use_openssl) { + $result = openssl_encrypt( + $data, + 'AES-128-CBC', + $aes_secret, + 0, + $iv + ); + } else { + $cipher = new Crypt\AES(Crypt\Base::MODE_CBC); + $cipher->setIV($iv); + $cipher->setKey($aes_secret); + $result = base64_encode($cipher->encrypt($data)); + } + $this->cleanSSLErrors(); + $iv = base64_encode($iv); + return json_encode( + [ + 'iv' => $iv, + 'mac' => hash_hmac('sha1', $iv . $result, $mac_secret), + 'payload' => $result, + ] + ); + } + + /** + * Decryption using openssl's AES or phpseclib's AES + * (phpseclib uses mcrypt when it is available) + * + * @param string $encdata encrypted data + * @param string $secret the secret + * + * @return string|false original data, false on error + */ + public function cookieDecrypt($encdata, $secret) + { + $data = json_decode($encdata, true); + + if (! is_array($data) || ! isset($data['mac']) || ! isset($data['iv']) || ! isset($data['payload']) + || ! is_string($data['mac']) || ! is_string($data['iv']) || ! is_string($data['payload']) + ) { + return false; + } + + $mac_secret = $this->getMACSecret($secret); + $aes_secret = $this->getAESSecret($secret); + $newmac = hash_hmac('sha1', $data['iv'] . $data['payload'], $mac_secret); + + if (! hash_equals($data['mac'], $newmac)) { + return false; + } + + if ($this->_use_openssl) { + $result = openssl_decrypt( + $data['payload'], + 'AES-128-CBC', + $aes_secret, + 0, + base64_decode($data['iv']) + ); + } else { + $cipher = new Crypt\AES(Crypt\Base::MODE_CBC); + $cipher->setIV(base64_decode($data['iv'])); + $cipher->setKey($aes_secret); + $result = $cipher->decrypt(base64_decode($data['payload'])); + } + $this->cleanSSLErrors(); + return $result; + } + + /** + * Returns size of IV for encryption. + * + * @return int + */ + public function getIVSize() + { + if ($this->_use_openssl) { + return openssl_cipher_iv_length('AES-128-CBC'); + } + return (new Crypt\AES(Crypt\Base::MODE_CBC))->block_size; + } + + /** + * Initialization + * Store the initialization vector because it will be needed for + * further decryption. I don't think necessary to have one iv + * per server so I don't put the server number in the cookie name. + * + * @return string + */ + public function createIV() + { + /* Testsuite shortcut only to allow predictable IV */ + if ($this->_cookie_iv !== null) { + return $this->_cookie_iv; + } + if ($this->_use_openssl) { + return openssl_random_pseudo_bytes( + $this->getIVSize() + ); + } + + return Crypt\Random::string( + $this->getIVSize() + ); + } + + /** + * Sets encryption IV to use + * + * This is for testing only! + * + * @param string $vector The IV + * + * @return void + */ + public function setIV($vector) + { + $this->_cookie_iv = $vector; + } + + /** + * Callback when user changes password. + * + * @param string $password New password to set + * + * @return void + */ + public function handlePasswordChange($password) + { + $this->storePasswordCookie($password); + } + + /** + * Perform logout + * + * @return void + */ + public function logOut() + { + /** @var Config $PMA_Config */ + global $PMA_Config; + + // -> delete password cookie(s) + if ($GLOBALS['cfg']['LoginCookieDeleteAll']) { + foreach ($GLOBALS['cfg']['Servers'] as $key => $val) { + $PMA_Config->removeCookie('pmaAuth-' . $key); + if ($PMA_Config->issetCookie('pmaAuth-' . $key)) { + $PMA_Config->removeCookie('pmaAuth-' . $key); + } + } + } else { + $cookieName = 'pmaAuth-' . $GLOBALS['server']; + $PMA_Config->removeCookie($cookieName); + if ($PMA_Config->issetCookie($cookieName)) { + $PMA_Config->removeCookie($cookieName); + } + } + parent::logOut(); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationHttp.php b/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationHttp.php new file mode 100644 index 0000000..6d735e8 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationHttp.php @@ -0,0 +1,214 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * HTTP Authentication plugin for phpMyAdmin. + * NOTE: Requires PHP loaded as a Apache module. + * + * @package PhpMyAdmin-Authentication + * @subpackage HTTP + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Plugins\Auth; + +use PhpMyAdmin\Config; +use PhpMyAdmin\Core; +use PhpMyAdmin\Message; +use PhpMyAdmin\Plugins\AuthenticationPlugin; +use PhpMyAdmin\Response; + +/** + * Handles the HTTP authentication methods + * + * @package PhpMyAdmin-Authentication + */ +class AuthenticationHttp extends AuthenticationPlugin +{ + /** + * Displays authentication form and redirect as necessary + * + * @return boolean always true (no return indeed) + */ + public function showLoginForm() + { + $response = Response::getInstance(); + if ($response->isAjax()) { + $response->setRequestStatus(false); + // reload_flag removes the token parameter from the URL and reloads + $response->addJSON('reload_flag', '1'); + if (defined('TESTSUITE')) { + return true; + } else { + exit; + } + } + + return $this->authForm(); + } + + /** + * Displays authentication form + * + * @return boolean + */ + public function authForm() + { + if (empty($GLOBALS['cfg']['Server']['auth_http_realm'])) { + if (empty($GLOBALS['cfg']['Server']['verbose'])) { + $server_message = $GLOBALS['cfg']['Server']['host']; + } else { + $server_message = $GLOBALS['cfg']['Server']['verbose']; + } + $realm_message = 'phpMyAdmin ' . $server_message; + } else { + $realm_message = $GLOBALS['cfg']['Server']['auth_http_realm']; + } + + $response = Response::getInstance(); + + // remove non US-ASCII to respect RFC2616 + $realm_message = preg_replace('/[^\x20-\x7e]/i', '', $realm_message); + $response->header('WWW-Authenticate: Basic realm="' . $realm_message . '"'); + $response->setHttpResponseCode(401); + + /* HTML header */ + $footer = $response->getFooter(); + $footer->setMinimal(); + $header = $response->getHeader(); + $header->setTitle(__('Access denied!')); + $header->disableMenuAndConsole(); + $header->setBodyId('loginform'); + + $response->addHTML('<h1>'); + $response->addHTML(sprintf(__('Welcome to %s'), ' phpMyAdmin')); + $response->addHTML('</h1>'); + $response->addHTML('<h3>'); + $response->addHTML( + Message::error( + __('Wrong username/password. Access denied.') + ) + ); + $response->addHTML('</h3>'); + + $response->addHTML(Config::renderFooter()); + + if (! defined('TESTSUITE')) { + exit; + } else { + return false; + } + } + + /** + * Gets authentication credentials + * + * @return boolean whether we get authentication settings or not + */ + public function readCredentials() + { + // Grabs the $PHP_AUTH_USER variable + if (isset($GLOBALS['PHP_AUTH_USER'])) { + $this->user = $GLOBALS['PHP_AUTH_USER']; + } + if (empty($this->user)) { + if (Core::getenv('PHP_AUTH_USER')) { + $this->user = Core::getenv('PHP_AUTH_USER'); + } elseif (Core::getenv('REMOTE_USER')) { + // CGI, might be encoded, see below + $this->user = Core::getenv('REMOTE_USER'); + } elseif (Core::getenv('REDIRECT_REMOTE_USER')) { + // CGI, might be encoded, see below + $this->user = Core::getenv('REDIRECT_REMOTE_USER'); + } elseif (Core::getenv('AUTH_USER')) { + // WebSite Professional + $this->user = Core::getenv('AUTH_USER'); + } elseif (Core::getenv('HTTP_AUTHORIZATION')) { + // IIS, might be encoded, see below + $this->user = Core::getenv('HTTP_AUTHORIZATION'); + } elseif (Core::getenv('Authorization')) { + // FastCGI, might be encoded, see below + $this->user = Core::getenv('Authorization'); + } + } + // Grabs the $PHP_AUTH_PW variable + if (isset($GLOBALS['PHP_AUTH_PW'])) { + $this->password = $GLOBALS['PHP_AUTH_PW']; + } + if (empty($this->password)) { + if (Core::getenv('PHP_AUTH_PW')) { + $this->password = Core::getenv('PHP_AUTH_PW'); + } elseif (Core::getenv('REMOTE_PASSWORD')) { + // Apache/CGI + $this->password = Core::getenv('REMOTE_PASSWORD'); + } elseif (Core::getenv('AUTH_PASSWORD')) { + // WebSite Professional + $this->password = Core::getenv('AUTH_PASSWORD'); + } + } + // Sanitize empty password login + if ($this->password === null) { + $this->password = ''; + } + + // Avoid showing the password in phpinfo()'s output + unset($GLOBALS['PHP_AUTH_PW']); + unset($_SERVER['PHP_AUTH_PW']); + + // Decode possibly encoded information (used by IIS/CGI/FastCGI) + // (do not use explode() because a user might have a colon in his password + if (strcmp(substr($this->user, 0, 6), 'Basic ') == 0) { + $usr_pass = base64_decode(substr($this->user, 6)); + if (! empty($usr_pass)) { + $colon = strpos($usr_pass, ':'); + if ($colon) { + $this->user = substr($usr_pass, 0, $colon); + $this->password = substr($usr_pass, $colon + 1); + } + unset($colon); + } + unset($usr_pass); + } + + // sanitize username + $this->user = Core::sanitizeMySQLUser($this->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) + && (isset($this->user) && hash_equals($old_usr, $this->user)) + ) { + $this->user = ''; + } + + // Returns whether we get authentication settings or not + return ! empty($this->user); + } + + /** + * User is not allowed to login to MySQL -> authentication failed + * + * @param string $failure String describing why authentication has failed + * + * @return void + */ + public function showFailure($failure) + { + parent::showFailure($failure); + $error = $GLOBALS['dbi']->getError(); + if ($error && $GLOBALS['errno'] != 1045) { + Core::fatalError($error); + } else { + $this->authForm(); + } + } + + /** + * Returns URL for login form. + * + * @return string + */ + public function getLoginFormURL() + { + return './index.php?old_usr=' . $this->user; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationSignon.php b/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationSignon.php new file mode 100644 index 0000000..36b1d66 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationSignon.php @@ -0,0 +1,282 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * SignOn Authentication plugin for phpMyAdmin + * + * @package PhpMyAdmin-Authentication + * @subpackage SignOn + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Plugins\Auth; + +use PhpMyAdmin\Core; +use PhpMyAdmin\Plugins\AuthenticationPlugin; +use PhpMyAdmin\Util; + +/** + * Handles the SignOn authentication method + * + * @package PhpMyAdmin-Authentication + */ +class AuthenticationSignon extends AuthenticationPlugin +{ + /** + * Displays authentication form + * + * @return boolean always true (no return indeed) + */ + public function showLoginForm() + { + unset($_SESSION['LAST_SIGNON_URL']); + if (empty($GLOBALS['cfg']['Server']['SignonURL'])) { + Core::fatalError('You must set SignonURL!'); + } else { + Core::sendHeaderLocation($GLOBALS['cfg']['Server']['SignonURL']); + } + + if (! defined('TESTSUITE')) { + exit; + } else { + return false; + } + } + + /** + * Set cookie params + * + * @param array $sessionCookieParams The cookie params + * @return void + */ + public function setCookieParams(array $sessionCookieParams = null): void + { + /* Session cookie params from config */ + if ($sessionCookieParams === null) { + $sessionCookieParams = (array) $GLOBALS['cfg']['Server']['SignonCookieParams']; + } + + /* Sanitize cookie params */ + $defaultCookieParams = function ($key) { + switch ($key) { + case 'lifetime': + return 0; + case 'path': + return '/'; + case 'domain': + return ''; + case 'secure': + return false; + case 'httponly': + return false; + } + return null; + }; + + foreach (['lifetime', 'path', 'domain', 'secure', 'httponly'] as $key) { + if (! isset($sessionCookieParams[$key])) { + $sessionCookieParams[$key] = $defaultCookieParams($key); + } + } + + if (isset($sessionCookieParams['samesite']) + && ! in_array($sessionCookieParams['samesite'], ['Lax', 'Strict'])) { + // Not a valid value for samesite + unset($sessionCookieParams['samesite']); + } + + if (version_compare(phpversion(), '7.3.0', '>=')) { + session_set_cookie_params($sessionCookieParams); + } + + session_set_cookie_params( + $sessionCookieParams['lifetime'], + $sessionCookieParams['path'], + $sessionCookieParams['domain'], + $sessionCookieParams['secure'], + $sessionCookieParams['httponly'] + ); + } + + /** + * Gets authentication credentials + * + * @return boolean whether we get authentication settings or not + */ + public function readCredentials() + { + /* Check if we're using same signon server */ + $signon_url = $GLOBALS['cfg']['Server']['SignonURL']; + if (isset($_SESSION['LAST_SIGNON_URL']) + && $_SESSION['LAST_SIGNON_URL'] != $signon_url + ) { + return false; + } + + /* Script name */ + $script_name = $GLOBALS['cfg']['Server']['SignonScript']; + + /* Session name */ + $session_name = $GLOBALS['cfg']['Server']['SignonSession']; + + /* Login URL */ + $signon_url = $GLOBALS['cfg']['Server']['SignonURL']; + + /* Current host */ + $single_signon_host = $GLOBALS['cfg']['Server']['host']; + + /* Current port */ + $single_signon_port = $GLOBALS['cfg']['Server']['port']; + + /* No configuration updates */ + $single_signon_cfgupdate = []; + + /* Handle script based auth */ + if (! empty($script_name)) { + if (! @file_exists($script_name)) { + Core::fatalError( + __('Can not find signon authentication script:') + . ' ' . $script_name + ); + } + include $script_name; + + list ($this->user, $this->password) + = get_login_credentials($GLOBALS['cfg']['Server']['user']); + } elseif (isset($_COOKIE[$session_name])) { /* Does session exist? */ + /* End current session */ + $old_session = session_name(); + $old_id = session_id(); + $oldCookieParams = session_get_cookie_params(); + if (! defined('TESTSUITE')) { + session_write_close(); + } + /* Load single signon session */ + if (! defined('TESTSUITE')) { + $this->setCookieParams(); + session_name($session_name); + session_id($_COOKIE[$session_name]); + session_start(); + } + + /* Clear error message */ + unset($_SESSION['PMA_single_signon_error_message']); + + /* Grab credentials if they exist */ + if (isset($_SESSION['PMA_single_signon_user'])) { + $this->user = $_SESSION['PMA_single_signon_user']; + } + if (isset($_SESSION['PMA_single_signon_password'])) { + $this->password = $_SESSION['PMA_single_signon_password']; + } + if (isset($_SESSION['PMA_single_signon_host'])) { + $single_signon_host = $_SESSION['PMA_single_signon_host']; + } + + if (isset($_SESSION['PMA_single_signon_port'])) { + $single_signon_port = $_SESSION['PMA_single_signon_port']; + } + + if (isset($_SESSION['PMA_single_signon_cfgupdate'])) { + $single_signon_cfgupdate = $_SESSION['PMA_single_signon_cfgupdate']; + } + + /* Also get token as it is needed to access subpages */ + if (isset($_SESSION['PMA_single_signon_token'])) { + /* No need to care about token on logout */ + $pma_token = $_SESSION['PMA_single_signon_token']; + } + + /* End single signon session */ + if (! defined('TESTSUITE')) { + session_write_close(); + } + + /* Restart phpMyAdmin session */ + if (! defined('TESTSUITE')) { + $this->setCookieParams($oldCookieParams); + session_name($old_session); + if (! empty($old_id)) { + session_id($old_id); + } + session_start(); + } + + /* Set the single signon host */ + $GLOBALS['cfg']['Server']['host'] = $single_signon_host; + + /* Set the single signon port */ + $GLOBALS['cfg']['Server']['port'] = $single_signon_port; + + /* Configuration update */ + $GLOBALS['cfg']['Server'] = array_merge( + $GLOBALS['cfg']['Server'], + $single_signon_cfgupdate + ); + + /* Restore our token */ + if (! empty($pma_token)) { + $_SESSION[' PMA_token '] = $pma_token; + $_SESSION[' HMAC_secret '] = Util::generateRandom(16); + } + + /** + * Clear user cache. + */ + Util::clearUserCache(); + } + + // Returns whether we get authentication settings or not + if (empty($this->user)) { + unset($_SESSION['LAST_SIGNON_URL']); + + return false; + } + + $_SESSION['LAST_SIGNON_URL'] = $GLOBALS['cfg']['Server']['SignonURL']; + + return true; + } + + /** + * User is not allowed to login to MySQL -> authentication failed + * + * @param string $failure String describing why authentication has failed + * + * @return void + */ + public function showFailure($failure) + { + parent::showFailure($failure); + + /* Session name */ + $session_name = $GLOBALS['cfg']['Server']['SignonSession']; + + /* Does session exist? */ + if (isset($_COOKIE[$session_name])) { + if (! defined('TESTSUITE')) { + /* End current session */ + session_write_close(); + + /* Load single signon session */ + $this->setCookieParams(); + session_name($session_name); + session_id($_COOKIE[$session_name]); + session_start(); + } + + /* Set error message */ + $_SESSION['PMA_single_signon_error_message'] = $this->getErrorMessage($failure); + } + $this->showLoginForm(); + } + + /** + * Returns URL for login form. + * + * @return string + */ + public function getLoginFormURL() + { + return $GLOBALS['cfg']['Server']['SignonURL']; + } +} |
