Merge branch 'origin/QA_5_1'
[phpmyadmin.git] / libraries / classes / Config / ServerConfigChecks.php
blob762fe681196836c115319b1e55917d7b7b69f943
1 <?php
2 /**
3 * Server config checks management
4 */
6 declare(strict_types=1);
8 namespace PhpMyAdmin\Config;
10 use PhpMyAdmin\Core;
11 use PhpMyAdmin\Sanitize;
12 use PhpMyAdmin\Setup\Index as SetupIndex;
13 use PhpMyAdmin\Url;
14 use PhpMyAdmin\Util;
16 use function count;
17 use function function_exists;
18 use function htmlspecialchars;
19 use function implode;
20 use function ini_get;
21 use function preg_match;
22 use function sprintf;
23 use function strlen;
25 /**
26 * Performs various compatibility, security and consistency checks on current config
28 * Outputs results to message list, must be called between SetupIndex::messagesBegin()
29 * and SetupIndex::messagesEnd()
31 class ServerConfigChecks
33 /** @var ConfigFile configurations being checked */
34 protected $cfg;
36 /**
37 * @param ConfigFile $cfg Configuration
39 public function __construct(ConfigFile $cfg)
41 $this->cfg = $cfg;
44 /**
45 * Perform config checks
47 * @return void
49 public function performConfigChecks()
51 $blowfishSecret = $this->cfg->get('blowfish_secret');
52 $blowfishSecretSet = false;
53 $cookieAuthUsed = false;
55 [$cookieAuthUsed, $blowfishSecret, $blowfishSecretSet]
56 = $this->performConfigChecksServers(
57 $cookieAuthUsed,
58 $blowfishSecret,
59 $blowfishSecretSet
62 $this->performConfigChecksCookieAuthUsed(
63 $cookieAuthUsed,
64 $blowfishSecretSet,
65 $blowfishSecret
68 // $cfg['AllowArbitraryServer']
69 // should be disabled
70 if ($this->cfg->getValue('AllowArbitraryServer')) {
71 $sAllowArbitraryServerWarn = sprintf(
72 __(
73 'This %soption%s should be disabled as it allows attackers to '
74 . 'bruteforce login to any MySQL server. If you feel this is necessary, '
75 . 'use %srestrict login to MySQL server%s or %strusted proxies list%s. '
76 . 'However, IP-based protection with trusted proxies list may not be '
77 . 'reliable if your IP belongs to an ISP where thousands of users, '
78 . 'including you, are connected to.'
80 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
81 '[/a]',
82 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
83 '[/a]',
84 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
85 '[/a]'
87 SetupIndex::messagesSet(
88 'notice',
89 'AllowArbitraryServer',
90 Descriptions::get('AllowArbitraryServer'),
91 Sanitize::sanitizeMessage($sAllowArbitraryServerWarn)
95 $this->performConfigChecksLoginCookie();
97 $sDirectoryNotice = __(
98 'This value should be double checked to ensure that this directory is '
99 . 'neither world accessible nor readable or writable by other users on '
100 . 'your server.'
103 // $cfg['SaveDir']
104 // should not be world-accessible
105 if ($this->cfg->getValue('SaveDir') != '') {
106 SetupIndex::messagesSet(
107 'notice',
108 'SaveDir',
109 Descriptions::get('SaveDir'),
110 Sanitize::sanitizeMessage($sDirectoryNotice)
114 // $cfg['TempDir']
115 // should not be world-accessible
116 if ($this->cfg->getValue('TempDir') != '') {
117 SetupIndex::messagesSet(
118 'notice',
119 'TempDir',
120 Descriptions::get('TempDir'),
121 Sanitize::sanitizeMessage($sDirectoryNotice)
125 $this->performConfigChecksZips();
129 * Check config of servers
131 * @param bool $cookieAuthUsed Cookie auth is used
132 * @param string $blowfishSecret Blowfish secret
133 * @param bool $blowfishSecretSet Blowfish secret set
135 * @return array
137 protected function performConfigChecksServers(
138 $cookieAuthUsed,
139 $blowfishSecret,
140 $blowfishSecretSet
142 $serverCnt = $this->cfg->getServerCount();
143 for ($i = 1; $i <= $serverCnt; $i++) {
144 $cookieAuthServer
145 = ($this->cfg->getValue('Servers/' . $i . '/auth_type') === 'cookie');
146 $cookieAuthUsed |= $cookieAuthServer;
147 $serverName = $this->performConfigChecksServersGetServerName(
148 $this->cfg->getServerName($i),
151 $serverName = htmlspecialchars($serverName);
153 [$blowfishSecret, $blowfishSecretSet]
154 = $this->performConfigChecksServersSetBlowfishSecret(
155 $blowfishSecret,
156 $cookieAuthServer,
157 $blowfishSecretSet
160 // $cfg['Servers'][$i]['ssl']
161 // should be enabled if possible
162 if (! $this->cfg->getValue('Servers/' . $i . '/ssl')) {
163 $title = Descriptions::get('Servers/1/ssl') . ' (' . $serverName . ')';
164 SetupIndex::messagesSet(
165 'notice',
166 'Servers/' . $i . '/ssl',
167 $title,
169 'You should use SSL connections if your database server '
170 . 'supports it.'
175 $sSecurityInfoMsg = Sanitize::sanitizeMessage(sprintf(
177 'If you feel this is necessary, use additional protection settings - '
178 . '%1$shost authentication%2$s settings and %3$strusted proxies list%4$s. '
179 . 'However, IP-based protection may not be reliable if your IP belongs '
180 . 'to an ISP where thousands of users, including you, are connected to.'
182 '[a@' . Url::getCommon(['page' => 'servers', 'mode' => 'edit', 'id' => $i]) . '#tab_Server_config]',
183 '[/a]',
184 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
185 '[/a]'
188 // $cfg['Servers'][$i]['auth_type']
189 // warn about full user credentials if 'auth_type' is 'config'
190 if (
191 $this->cfg->getValue('Servers/' . $i . '/auth_type') === 'config'
192 && $this->cfg->getValue('Servers/' . $i . '/user') != ''
193 && $this->cfg->getValue('Servers/' . $i . '/password') != ''
195 $title = Descriptions::get('Servers/1/auth_type')
196 . ' (' . $serverName . ')';
197 SetupIndex::messagesSet(
198 'notice',
199 'Servers/' . $i . '/auth_type',
200 $title,
201 Sanitize::sanitizeMessage(sprintf(
203 'You set the [kbd]config[/kbd] authentication type and included '
204 . 'username and password for auto-login, which is not a desirable '
205 . 'option for live hosts. Anyone who knows or guesses your phpMyAdmin '
206 . 'URL can directly access your phpMyAdmin panel. Set %1$sauthentication '
207 . 'type%2$s to [kbd]cookie[/kbd] or [kbd]http[/kbd].'
209 '[a@' . Url::getCommon(['page' => 'servers', 'mode' => 'edit', 'id' => $i]) . '#tab_Server]',
210 '[/a]'
212 . ' ' . $sSecurityInfoMsg
216 // $cfg['Servers'][$i]['AllowRoot']
217 // $cfg['Servers'][$i]['AllowNoPassword']
218 // serious security flaw
219 if (
220 ! $this->cfg->getValue('Servers/' . $i . '/AllowRoot')
221 || ! $this->cfg->getValue('Servers/' . $i . '/AllowNoPassword')
223 continue;
226 $title = Descriptions::get('Servers/1/AllowNoPassword')
227 . ' (' . $serverName . ')';
228 SetupIndex::messagesSet(
229 'notice',
230 'Servers/' . $i . '/AllowNoPassword',
231 $title,
232 __('You allow for connecting to the server without a password.')
233 . ' ' . $sSecurityInfoMsg
237 return [
238 $cookieAuthUsed,
239 $blowfishSecret,
240 $blowfishSecretSet,
245 * Set blowfish secret
247 * @param string|null $blowfishSecret Blowfish secret
248 * @param bool $cookieAuthServer Cookie auth is used
249 * @param bool $blowfishSecretSet Blowfish secret set
251 * @return array
253 protected function performConfigChecksServersSetBlowfishSecret(
254 $blowfishSecret,
255 $cookieAuthServer,
256 $blowfishSecretSet
257 ): array {
258 if ($cookieAuthServer && $blowfishSecret === null) {
259 $blowfishSecretSet = true;
260 $this->cfg->set('blowfish_secret', Util::generateRandom(32));
263 return [
264 $blowfishSecret,
265 $blowfishSecretSet,
270 * Define server name
272 * @param string $serverName Server name
273 * @param int $serverId Server id
275 * @return string Server name
277 protected function performConfigChecksServersGetServerName(
278 $serverName,
279 $serverId
281 if ($serverName === 'localhost') {
282 return $serverName . ' [' . $serverId . ']';
285 return $serverName;
289 * Perform config checks for zip part.
291 * @return void
293 protected function performConfigChecksZips()
295 $this->performConfigChecksServerGZipdump();
296 $this->performConfigChecksServerBZipdump();
297 $this->performConfigChecksServersZipdump();
301 * Perform config checks for zip part.
303 * @return void
305 protected function performConfigChecksServersZipdump()
307 // $cfg['ZipDump']
308 // requires zip_open in import
309 if ($this->cfg->getValue('ZipDump') && ! $this->functionExists('zip_open')) {
310 SetupIndex::messagesSet(
311 'error',
312 'ZipDump_import',
313 Descriptions::get('ZipDump'),
314 Sanitize::sanitizeMessage(sprintf(
316 '%sZip decompression%s requires functions (%s) which are unavailable '
317 . 'on this system.'
319 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]',
320 '[/a]',
321 'zip_open'
326 // $cfg['ZipDump']
327 // requires gzcompress in export
328 if (! $this->cfg->getValue('ZipDump') || $this->functionExists('gzcompress')) {
329 return;
332 SetupIndex::messagesSet(
333 'error',
334 'ZipDump_export',
335 Descriptions::get('ZipDump'),
336 Sanitize::sanitizeMessage(sprintf(
338 '%sZip compression%s requires functions (%s) which are unavailable on '
339 . 'this system.'
341 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]',
342 '[/a]',
343 'gzcompress'
349 * Check config of servers
351 * @param bool $cookieAuthUsed Cookie auth is used
352 * @param bool $blowfishSecretSet Blowfish secret set
353 * @param string $blowfishSecret Blowfish secret
355 * @return void
357 protected function performConfigChecksCookieAuthUsed(
358 $cookieAuthUsed,
359 $blowfishSecretSet,
360 $blowfishSecret
362 // $cfg['blowfish_secret']
363 // it's required for 'cookie' authentication
364 if (! $cookieAuthUsed) {
365 return;
368 if ($blowfishSecretSet) {
369 // 'cookie' auth used, blowfish_secret was generated
370 SetupIndex::messagesSet(
371 'notice',
372 'blowfish_secret_created',
373 Descriptions::get('blowfish_secret'),
374 Sanitize::sanitizeMessage(__(
375 'You didn\'t have blowfish secret set and have enabled '
376 . '[kbd]cookie[/kbd] authentication, so a key was automatically '
377 . 'generated for you. It is used to encrypt cookies; you don\'t need to '
378 . 'remember it.'
381 } else {
382 $blowfishWarnings = [];
383 // check length
384 if (strlen($blowfishSecret) < 32) {
385 // too short key
386 $blowfishWarnings[] = __(
387 'Key is too short, it should have at least 32 characters.'
391 // check used characters
392 $hasDigits = (bool) preg_match('/\d/', $blowfishSecret);
393 $hasChars = (bool) preg_match('/\S/', $blowfishSecret);
394 $hasNonword = (bool) preg_match('/\W/', $blowfishSecret);
395 if (! $hasDigits || ! $hasChars || ! $hasNonword) {
396 $blowfishWarnings[] = Sanitize::sanitizeMessage(
398 'Key should contain letters, numbers [em]and[/em] '
399 . 'special characters.'
404 if (! empty($blowfishWarnings)) {
405 SetupIndex::messagesSet(
406 'error',
407 'blowfish_warnings' . count($blowfishWarnings),
408 Descriptions::get('blowfish_secret'),
409 implode('<br>', $blowfishWarnings)
416 * Check configuration for login cookie
418 * @return void
420 protected function performConfigChecksLoginCookie()
422 // $cfg['LoginCookieValidity']
423 // value greater than session.gc_maxlifetime will cause
424 // random session invalidation after that time
425 $loginCookieValidity = $this->cfg->getValue('LoginCookieValidity');
426 if (
427 $loginCookieValidity > ini_get('session.gc_maxlifetime')
429 SetupIndex::messagesSet(
430 'error',
431 'LoginCookieValidity',
432 Descriptions::get('LoginCookieValidity'),
433 Sanitize::sanitizeMessage(sprintf(
435 '%1$sLogin cookie validity%2$s greater than %3$ssession.gc_maxlifetime%4$s may '
436 . 'cause random session invalidation (currently session.gc_maxlifetime '
437 . 'is %5$d).'
439 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
440 '[/a]',
441 '[a@' . Core::getPHPDocLink('session.configuration.php#ini.session.gc-maxlifetime') . ']',
442 '[/a]',
443 ini_get('session.gc_maxlifetime')
448 // $cfg['LoginCookieValidity']
449 // should be at most 1800 (30 min)
450 if ($loginCookieValidity > 1800) {
451 SetupIndex::messagesSet(
452 'notice',
453 'LoginCookieValidity',
454 Descriptions::get('LoginCookieValidity'),
455 Sanitize::sanitizeMessage(sprintf(
457 '%sLogin cookie validity%s should be set to 1800 seconds (30 minutes) '
458 . 'at most. Values larger than 1800 may pose a security risk such as '
459 . 'impersonation.'
461 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
462 '[/a]'
467 // $cfg['LoginCookieValidity']
468 // $cfg['LoginCookieStore']
469 // LoginCookieValidity must be less or equal to LoginCookieStore
470 if (
471 ($this->cfg->getValue('LoginCookieStore') == 0)
472 || ($loginCookieValidity <= $this->cfg->getValue('LoginCookieStore'))
474 return;
477 SetupIndex::messagesSet(
478 'error',
479 'LoginCookieValidity',
480 Descriptions::get('LoginCookieValidity'),
481 Sanitize::sanitizeMessage(sprintf(
483 'If using [kbd]cookie[/kbd] authentication and %sLogin cookie store%s '
484 . 'is not 0, %sLogin cookie validity%s must be set to a value less or '
485 . 'equal to it.'
487 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
488 '[/a]',
489 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
490 '[/a]'
496 * Check GZipDump configuration
498 * @return void
500 protected function performConfigChecksServerBZipdump()
502 // $cfg['BZipDump']
503 // requires bzip2 functions
504 if (
505 ! $this->cfg->getValue('BZipDump')
506 || ($this->functionExists('bzopen') && $this->functionExists('bzcompress'))
508 return;
511 $functions = $this->functionExists('bzopen')
512 ? '' :
513 'bzopen';
514 $functions .= $this->functionExists('bzcompress')
515 ? ''
516 : ($functions ? ', ' : '') . 'bzcompress';
517 SetupIndex::messagesSet(
518 'error',
519 'BZipDump',
520 Descriptions::get('BZipDump'),
521 Sanitize::sanitizeMessage(
522 sprintf(
524 '%1$sBzip2 compression and decompression%2$s requires functions (%3$s) which '
525 . 'are unavailable on this system.'
527 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]',
528 '[/a]',
529 $functions
536 * Check GZipDump configuration
538 * @return void
540 protected function performConfigChecksServerGZipdump()
542 // $cfg['GZipDump']
543 // requires zlib functions
544 if (
545 ! $this->cfg->getValue('GZipDump')
546 || ($this->functionExists('gzopen') && $this->functionExists('gzencode'))
548 return;
551 SetupIndex::messagesSet(
552 'error',
553 'GZipDump',
554 Descriptions::get('GZipDump'),
555 Sanitize::sanitizeMessage(sprintf(
557 '%1$sGZip compression and decompression%2$s requires functions (%3$s) which '
558 . 'are unavailable on this system.'
560 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]',
561 '[/a]',
562 'gzencode'
568 * Wrapper around function_exists to allow mock in test
570 * @param string $name Function name
572 * @return bool
574 protected function functionExists($name)
576 return function_exists($name);