You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
553 lines
19 KiB
553 lines
19 KiB
<?php |
|
/** |
|
* Server config checks management |
|
*/ |
|
|
|
declare(strict_types=1); |
|
|
|
namespace PhpMyAdmin\Config; |
|
|
|
use PhpMyAdmin\Core; |
|
use PhpMyAdmin\Sanitize; |
|
use PhpMyAdmin\Setup\Index as SetupIndex; |
|
use PhpMyAdmin\Url; |
|
use PhpMyAdmin\Util; |
|
|
|
use function __; |
|
use function count; |
|
use function function_exists; |
|
use function htmlspecialchars; |
|
use function implode; |
|
use function ini_get; |
|
use function preg_match; |
|
use function sprintf; |
|
use function strlen; |
|
|
|
/** |
|
* Performs various compatibility, security and consistency checks on current config |
|
* |
|
* Outputs results to message list, must be called between SetupIndex::messagesBegin() |
|
* and SetupIndex::messagesEnd() |
|
*/ |
|
class ServerConfigChecks |
|
{ |
|
/** @var ConfigFile configurations being checked */ |
|
protected $cfg; |
|
|
|
/** |
|
* @param ConfigFile $cfg Configuration |
|
*/ |
|
public function __construct(ConfigFile $cfg) |
|
{ |
|
$this->cfg = $cfg; |
|
} |
|
|
|
/** |
|
* Perform config checks |
|
*/ |
|
public function performConfigChecks(): void |
|
{ |
|
$blowfishSecret = $this->cfg->get('blowfish_secret'); |
|
$blowfishSecretSet = false; |
|
$cookieAuthUsed = false; |
|
|
|
[$cookieAuthUsed, $blowfishSecret, $blowfishSecretSet] = $this->performConfigChecksServers( |
|
$cookieAuthUsed, |
|
$blowfishSecret, |
|
$blowfishSecretSet |
|
); |
|
|
|
$this->performConfigChecksCookieAuthUsed($cookieAuthUsed, $blowfishSecretSet, $blowfishSecret); |
|
|
|
// $cfg['AllowArbitraryServer'] |
|
// should be disabled |
|
if ($this->cfg->getValue('AllowArbitraryServer')) { |
|
$sAllowArbitraryServerWarn = sprintf( |
|
__( |
|
'This %soption%s should be disabled as it allows attackers to ' |
|
. 'bruteforce login to any MySQL server. If you feel this is necessary, ' |
|
. 'use %srestrict login to MySQL server%s or %strusted proxies list%s. ' |
|
. 'However, IP-based protection with trusted proxies list may not be ' |
|
. 'reliable if your IP belongs to an ISP where thousands of users, ' |
|
. 'including you, are connected to.' |
|
), |
|
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', |
|
'[/a]', |
|
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', |
|
'[/a]', |
|
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', |
|
'[/a]' |
|
); |
|
SetupIndex::messagesSet( |
|
'notice', |
|
'AllowArbitraryServer', |
|
Descriptions::get('AllowArbitraryServer'), |
|
Sanitize::sanitizeMessage($sAllowArbitraryServerWarn) |
|
); |
|
} |
|
|
|
$this->performConfigChecksLoginCookie(); |
|
|
|
$sDirectoryNotice = __( |
|
'This value should be double checked to ensure that this directory is ' |
|
. 'neither world accessible nor readable or writable by other users on ' |
|
. 'your server.' |
|
); |
|
|
|
// $cfg['SaveDir'] |
|
// should not be world-accessible |
|
if ($this->cfg->getValue('SaveDir') != '') { |
|
SetupIndex::messagesSet( |
|
'notice', |
|
'SaveDir', |
|
Descriptions::get('SaveDir'), |
|
Sanitize::sanitizeMessage($sDirectoryNotice) |
|
); |
|
} |
|
|
|
// $cfg['TempDir'] |
|
// should not be world-accessible |
|
if ($this->cfg->getValue('TempDir') != '') { |
|
SetupIndex::messagesSet( |
|
'notice', |
|
'TempDir', |
|
Descriptions::get('TempDir'), |
|
Sanitize::sanitizeMessage($sDirectoryNotice) |
|
); |
|
} |
|
|
|
$this->performConfigChecksZips(); |
|
} |
|
|
|
/** |
|
* Check config of servers |
|
* |
|
* @param bool $cookieAuthUsed Cookie auth is used |
|
* @param string $blowfishSecret Blowfish secret |
|
* @param bool $blowfishSecretSet Blowfish secret set |
|
* |
|
* @return array |
|
*/ |
|
protected function performConfigChecksServers( |
|
$cookieAuthUsed, |
|
$blowfishSecret, |
|
$blowfishSecretSet |
|
) { |
|
$serverCnt = $this->cfg->getServerCount(); |
|
$isCookieAuthUsed = (int) $cookieAuthUsed; |
|
for ($i = 1; $i <= $serverCnt; $i++) { |
|
$cookieAuthServer = ($this->cfg->getValue('Servers/' . $i . '/auth_type') === 'cookie'); |
|
$isCookieAuthUsed |= (int) $cookieAuthServer; |
|
$serverName = $this->performConfigChecksServersGetServerName( |
|
$this->cfg->getServerName($i), |
|
$i |
|
); |
|
$serverName = htmlspecialchars($serverName); |
|
|
|
[$blowfishSecret, $blowfishSecretSet] = $this->performConfigChecksServersSetBlowfishSecret( |
|
$blowfishSecret, |
|
$cookieAuthServer, |
|
$blowfishSecretSet |
|
); |
|
|
|
// $cfg['Servers'][$i]['ssl'] |
|
// should be enabled if possible |
|
if (! $this->cfg->getValue('Servers/' . $i . '/ssl')) { |
|
$title = Descriptions::get('Servers/1/ssl') . ' (' . $serverName . ')'; |
|
SetupIndex::messagesSet( |
|
'notice', |
|
'Servers/' . $i . '/ssl', |
|
$title, |
|
__( |
|
'You should use SSL connections if your database server supports it.' |
|
) |
|
); |
|
} |
|
|
|
$sSecurityInfoMsg = Sanitize::sanitizeMessage(sprintf( |
|
__( |
|
'If you feel this is necessary, use additional protection settings - ' |
|
. '%1$shost authentication%2$s settings and %3$strusted proxies list%4$s. ' |
|
. 'However, IP-based protection may not be reliable if your IP belongs ' |
|
. 'to an ISP where thousands of users, including you, are connected to.' |
|
), |
|
'[a@' . Url::getCommon(['page' => 'servers', 'mode' => 'edit', 'id' => $i]) . '#tab_Server_config]', |
|
'[/a]', |
|
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', |
|
'[/a]' |
|
)); |
|
|
|
// $cfg['Servers'][$i]['auth_type'] |
|
// warn about full user credentials if 'auth_type' is 'config' |
|
if ( |
|
$this->cfg->getValue('Servers/' . $i . '/auth_type') === 'config' |
|
&& $this->cfg->getValue('Servers/' . $i . '/user') != '' |
|
&& $this->cfg->getValue('Servers/' . $i . '/password') != '' |
|
) { |
|
$title = Descriptions::get('Servers/1/auth_type') |
|
. ' (' . $serverName . ')'; |
|
SetupIndex::messagesSet( |
|
'notice', |
|
'Servers/' . $i . '/auth_type', |
|
$title, |
|
Sanitize::sanitizeMessage(sprintf( |
|
__( |
|
'You set the [kbd]config[/kbd] authentication type and included ' |
|
. 'username and password for auto-login, which is not a desirable ' |
|
. 'option for live hosts. Anyone who knows or guesses your phpMyAdmin ' |
|
. 'URL can directly access your phpMyAdmin panel. Set %1$sauthentication ' |
|
. 'type%2$s to [kbd]cookie[/kbd] or [kbd]http[/kbd].' |
|
), |
|
'[a@' . Url::getCommon(['page' => 'servers', 'mode' => 'edit', 'id' => $i]) . '#tab_Server]', |
|
'[/a]' |
|
)) |
|
. ' ' . $sSecurityInfoMsg |
|
); |
|
} |
|
|
|
// $cfg['Servers'][$i]['AllowRoot'] |
|
// $cfg['Servers'][$i]['AllowNoPassword'] |
|
// serious security flaw |
|
if ( |
|
! $this->cfg->getValue('Servers/' . $i . '/AllowRoot') |
|
|| ! $this->cfg->getValue('Servers/' . $i . '/AllowNoPassword') |
|
) { |
|
continue; |
|
} |
|
|
|
$title = Descriptions::get('Servers/1/AllowNoPassword') |
|
. ' (' . $serverName . ')'; |
|
SetupIndex::messagesSet( |
|
'notice', |
|
'Servers/' . $i . '/AllowNoPassword', |
|
$title, |
|
__('You allow for connecting to the server without a password.') |
|
. ' ' . $sSecurityInfoMsg |
|
); |
|
} |
|
|
|
return [ |
|
(bool) $isCookieAuthUsed, |
|
$blowfishSecret, |
|
$blowfishSecretSet, |
|
]; |
|
} |
|
|
|
/** |
|
* Set blowfish secret |
|
* |
|
* @param string|null $blowfishSecret Blowfish secret |
|
* @param bool $cookieAuthServer Cookie auth is used |
|
* @param bool $blowfishSecretSet Blowfish secret set |
|
* |
|
* @return array |
|
*/ |
|
protected function performConfigChecksServersSetBlowfishSecret( |
|
$blowfishSecret, |
|
$cookieAuthServer, |
|
$blowfishSecretSet |
|
): array { |
|
if ($cookieAuthServer && $blowfishSecret === null) { |
|
$blowfishSecretSet = true; |
|
$this->cfg->set('blowfish_secret', Util::generateRandom(32)); |
|
} |
|
|
|
return [ |
|
$blowfishSecret, |
|
$blowfishSecretSet, |
|
]; |
|
} |
|
|
|
/** |
|
* Define server name |
|
* |
|
* @param string $serverName Server name |
|
* @param int $serverId Server id |
|
* |
|
* @return string Server name |
|
*/ |
|
protected function performConfigChecksServersGetServerName( |
|
$serverName, |
|
$serverId |
|
) { |
|
if ($serverName === 'localhost') { |
|
return $serverName . ' [' . $serverId . ']'; |
|
} |
|
|
|
return $serverName; |
|
} |
|
|
|
/** |
|
* Perform config checks for zip part. |
|
*/ |
|
protected function performConfigChecksZips(): void |
|
{ |
|
$this->performConfigChecksServerGZipdump(); |
|
$this->performConfigChecksServerBZipdump(); |
|
$this->performConfigChecksServersZipdump(); |
|
} |
|
|
|
/** |
|
* Perform config checks for zip part. |
|
*/ |
|
protected function performConfigChecksServersZipdump(): void |
|
{ |
|
// $cfg['ZipDump'] |
|
// requires zip_open in import |
|
if ($this->cfg->getValue('ZipDump') && ! $this->functionExists('zip_open')) { |
|
SetupIndex::messagesSet( |
|
'error', |
|
'ZipDump_import', |
|
Descriptions::get('ZipDump'), |
|
Sanitize::sanitizeMessage(sprintf( |
|
__( |
|
'%sZip decompression%s requires functions (%s) which are unavailable on this system.' |
|
), |
|
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]', |
|
'[/a]', |
|
'zip_open' |
|
)) |
|
); |
|
} |
|
|
|
// $cfg['ZipDump'] |
|
// requires gzcompress in export |
|
if (! $this->cfg->getValue('ZipDump') || $this->functionExists('gzcompress')) { |
|
return; |
|
} |
|
|
|
SetupIndex::messagesSet( |
|
'error', |
|
'ZipDump_export', |
|
Descriptions::get('ZipDump'), |
|
Sanitize::sanitizeMessage(sprintf( |
|
__( |
|
'%sZip compression%s requires functions (%s) which are unavailable on this system.' |
|
), |
|
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]', |
|
'[/a]', |
|
'gzcompress' |
|
)) |
|
); |
|
} |
|
|
|
/** |
|
* Check config of servers |
|
* |
|
* @param bool $cookieAuthUsed Cookie auth is used |
|
* @param bool $blowfishSecretSet Blowfish secret set |
|
* @param string $blowfishSecret Blowfish secret |
|
*/ |
|
protected function performConfigChecksCookieAuthUsed( |
|
$cookieAuthUsed, |
|
$blowfishSecretSet, |
|
$blowfishSecret |
|
): void { |
|
// $cfg['blowfish_secret'] |
|
// it's required for 'cookie' authentication |
|
if (! $cookieAuthUsed) { |
|
return; |
|
} |
|
|
|
if ($blowfishSecretSet) { |
|
// 'cookie' auth used, blowfish_secret was generated |
|
SetupIndex::messagesSet( |
|
'notice', |
|
'blowfish_secret_created', |
|
Descriptions::get('blowfish_secret'), |
|
Sanitize::sanitizeMessage(__( |
|
'You didn\'t have blowfish secret set and have enabled ' |
|
. '[kbd]cookie[/kbd] authentication, so a key was automatically ' |
|
. 'generated for you. It is used to encrypt cookies; you don\'t need to ' |
|
. 'remember it.' |
|
)) |
|
); |
|
|
|
return; |
|
} |
|
|
|
$blowfishWarnings = []; |
|
// check length |
|
if (strlen($blowfishSecret) < 32) { |
|
// too short key |
|
$blowfishWarnings[] = __('Key is too short, it should have at least 32 characters.'); |
|
} |
|
|
|
// check used characters |
|
$hasDigits = (bool) preg_match('/\d/', $blowfishSecret); |
|
$hasChars = (bool) preg_match('/\S/', $blowfishSecret); |
|
$hasNonword = (bool) preg_match('/\W/', $blowfishSecret); |
|
if (! $hasDigits || ! $hasChars || ! $hasNonword) { |
|
$blowfishWarnings[] = Sanitize::sanitizeMessage( |
|
__( |
|
'Key should contain letters, numbers [em]and[/em] special characters.' |
|
) |
|
); |
|
} |
|
|
|
if (empty($blowfishWarnings)) { |
|
return; |
|
} |
|
|
|
SetupIndex::messagesSet( |
|
'error', |
|
'blowfish_warnings' . count($blowfishWarnings), |
|
Descriptions::get('blowfish_secret'), |
|
implode('<br>', $blowfishWarnings) |
|
); |
|
} |
|
|
|
/** |
|
* Check configuration for login cookie |
|
*/ |
|
protected function performConfigChecksLoginCookie(): void |
|
{ |
|
// $cfg['LoginCookieValidity'] |
|
// value greater than session.gc_maxlifetime will cause |
|
// random session invalidation after that time |
|
$loginCookieValidity = $this->cfg->getValue('LoginCookieValidity'); |
|
if ($loginCookieValidity > ini_get('session.gc_maxlifetime')) { |
|
SetupIndex::messagesSet( |
|
'error', |
|
'LoginCookieValidity', |
|
Descriptions::get('LoginCookieValidity'), |
|
Sanitize::sanitizeMessage(sprintf( |
|
__( |
|
'%1$sLogin cookie validity%2$s greater than %3$ssession.gc_maxlifetime%4$s may ' |
|
. 'cause random session invalidation (currently session.gc_maxlifetime ' |
|
. 'is %5$d).' |
|
), |
|
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', |
|
'[/a]', |
|
'[a@' . Core::getPHPDocLink('session.configuration.php#ini.session.gc-maxlifetime') . ']', |
|
'[/a]', |
|
ini_get('session.gc_maxlifetime') |
|
)) |
|
); |
|
} |
|
|
|
// $cfg['LoginCookieValidity'] |
|
// should be at most 1800 (30 min) |
|
if ($loginCookieValidity > 1800) { |
|
SetupIndex::messagesSet( |
|
'notice', |
|
'LoginCookieValidity', |
|
Descriptions::get('LoginCookieValidity'), |
|
Sanitize::sanitizeMessage(sprintf( |
|
__( |
|
'%sLogin cookie validity%s should be set to 1800 seconds (30 minutes) ' |
|
. 'at most. Values larger than 1800 may pose a security risk such as ' |
|
. 'impersonation.' |
|
), |
|
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', |
|
'[/a]' |
|
)) |
|
); |
|
} |
|
|
|
// $cfg['LoginCookieValidity'] |
|
// $cfg['LoginCookieStore'] |
|
// LoginCookieValidity must be less or equal to LoginCookieStore |
|
if ( |
|
($this->cfg->getValue('LoginCookieStore') == 0) |
|
|| ($loginCookieValidity <= $this->cfg->getValue('LoginCookieStore')) |
|
) { |
|
return; |
|
} |
|
|
|
SetupIndex::messagesSet( |
|
'error', |
|
'LoginCookieValidity', |
|
Descriptions::get('LoginCookieValidity'), |
|
Sanitize::sanitizeMessage(sprintf( |
|
__( |
|
'If using [kbd]cookie[/kbd] authentication and %sLogin cookie store%s ' |
|
. 'is not 0, %sLogin cookie validity%s must be set to a value less or ' |
|
. 'equal to it.' |
|
), |
|
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', |
|
'[/a]', |
|
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', |
|
'[/a]' |
|
)) |
|
); |
|
} |
|
|
|
/** |
|
* Check GZipDump configuration |
|
*/ |
|
protected function performConfigChecksServerBZipdump(): void |
|
{ |
|
// $cfg['BZipDump'] |
|
// requires bzip2 functions |
|
if ( |
|
! $this->cfg->getValue('BZipDump') |
|
|| ($this->functionExists('bzopen') && $this->functionExists('bzcompress')) |
|
) { |
|
return; |
|
} |
|
|
|
$functions = $this->functionExists('bzopen') |
|
? '' : |
|
'bzopen'; |
|
$functions .= $this->functionExists('bzcompress') |
|
? '' |
|
: ($functions ? ', ' : '') . 'bzcompress'; |
|
SetupIndex::messagesSet( |
|
'error', |
|
'BZipDump', |
|
Descriptions::get('BZipDump'), |
|
Sanitize::sanitizeMessage( |
|
sprintf( |
|
__( |
|
'%1$sBzip2 compression and decompression%2$s requires functions (%3$s) which ' |
|
. 'are unavailable on this system.' |
|
), |
|
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]', |
|
'[/a]', |
|
$functions |
|
) |
|
) |
|
); |
|
} |
|
|
|
/** |
|
* Check GZipDump configuration |
|
*/ |
|
protected function performConfigChecksServerGZipdump(): void |
|
{ |
|
// $cfg['GZipDump'] |
|
// requires zlib functions |
|
if ( |
|
! $this->cfg->getValue('GZipDump') |
|
|| ($this->functionExists('gzopen') && $this->functionExists('gzencode')) |
|
) { |
|
return; |
|
} |
|
|
|
SetupIndex::messagesSet( |
|
'error', |
|
'GZipDump', |
|
Descriptions::get('GZipDump'), |
|
Sanitize::sanitizeMessage(sprintf( |
|
__( |
|
'%1$sGZip compression and decompression%2$s requires functions (%3$s) which ' |
|
. 'are unavailable on this system.' |
|
), |
|
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]', |
|
'[/a]', |
|
'gzencode' |
|
)) |
|
); |
|
} |
|
|
|
/** |
|
* Wrapper around function_exists to allow mock in test |
|
* |
|
* @param string $name Function name |
|
*/ |
|
protected function functionExists($name): bool |
|
{ |
|
return function_exists($name); |
|
} |
|
}
|
|
|