1<?php 2/** 3 * Server config checks management 4 */ 5 6declare(strict_types=1); 7 8namespace PhpMyAdmin\Config; 9 10use PhpMyAdmin\Core; 11use PhpMyAdmin\Sanitize; 12use PhpMyAdmin\Setup\Index as SetupIndex; 13use PhpMyAdmin\Url; 14use PhpMyAdmin\Util; 15use function count; 16use function function_exists; 17use function htmlspecialchars; 18use function implode; 19use function ini_get; 20use function preg_match; 21use function sprintf; 22use function strlen; 23 24/** 25 * Performs various compatibility, security and consistency checks on current config 26 * 27 * Outputs results to message list, must be called between SetupIndex::messagesBegin() 28 * and SetupIndex::messagesEnd() 29 */ 30class ServerConfigChecks 31{ 32 /** @var ConfigFile configurations being checked */ 33 protected $cfg; 34 35 /** 36 * @param ConfigFile $cfg Configuration 37 */ 38 public function __construct(ConfigFile $cfg) 39 { 40 $this->cfg = $cfg; 41 } 42 43 /** 44 * Perform config checks 45 * 46 * @return void 47 */ 48 public function performConfigChecks() 49 { 50 $blowfishSecret = $this->cfg->get('blowfish_secret'); 51 $blowfishSecretSet = false; 52 $cookieAuthUsed = false; 53 54 [$cookieAuthUsed, $blowfishSecret, $blowfishSecretSet] 55 = $this->performConfigChecksServers( 56 $cookieAuthUsed, 57 $blowfishSecret, 58 $blowfishSecretSet 59 ); 60 61 $this->performConfigChecksCookieAuthUsed( 62 $cookieAuthUsed, 63 $blowfishSecretSet, 64 $blowfishSecret 65 ); 66 67 // $cfg['AllowArbitraryServer'] 68 // should be disabled 69 if ($this->cfg->getValue('AllowArbitraryServer')) { 70 $sAllowArbitraryServerWarn = sprintf( 71 __( 72 'This %soption%s should be disabled as it allows attackers to ' 73 . 'bruteforce login to any MySQL server. If you feel this is necessary, ' 74 . 'use %srestrict login to MySQL server%s or %strusted proxies list%s. ' 75 . 'However, IP-based protection with trusted proxies list may not be ' 76 . 'reliable if your IP belongs to an ISP where thousands of users, ' 77 . 'including you, are connected to.' 78 ), 79 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', 80 '[/a]', 81 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', 82 '[/a]', 83 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', 84 '[/a]' 85 ); 86 SetupIndex::messagesSet( 87 'notice', 88 'AllowArbitraryServer', 89 Descriptions::get('AllowArbitraryServer'), 90 Sanitize::sanitizeMessage($sAllowArbitraryServerWarn) 91 ); 92 } 93 94 $this->performConfigChecksLoginCookie(); 95 96 $sDirectoryNotice = __( 97 'This value should be double checked to ensure that this directory is ' 98 . 'neither world accessible nor readable or writable by other users on ' 99 . 'your server.' 100 ); 101 102 // $cfg['SaveDir'] 103 // should not be world-accessible 104 if ($this->cfg->getValue('SaveDir') != '') { 105 SetupIndex::messagesSet( 106 'notice', 107 'SaveDir', 108 Descriptions::get('SaveDir'), 109 Sanitize::sanitizeMessage($sDirectoryNotice) 110 ); 111 } 112 113 // $cfg['TempDir'] 114 // should not be world-accessible 115 if ($this->cfg->getValue('TempDir') != '') { 116 SetupIndex::messagesSet( 117 'notice', 118 'TempDir', 119 Descriptions::get('TempDir'), 120 Sanitize::sanitizeMessage($sDirectoryNotice) 121 ); 122 } 123 124 $this->performConfigChecksZips(); 125 } 126 127 /** 128 * Check config of servers 129 * 130 * @param bool $cookieAuthUsed Cookie auth is used 131 * @param string $blowfishSecret Blowfish secret 132 * @param bool $blowfishSecretSet Blowfish secret set 133 * 134 * @return array 135 */ 136 protected function performConfigChecksServers( 137 $cookieAuthUsed, 138 $blowfishSecret, 139 $blowfishSecretSet 140 ) { 141 $serverCnt = $this->cfg->getServerCount(); 142 for ($i = 1; $i <= $serverCnt; $i++) { 143 $cookieAuthServer 144 = ($this->cfg->getValue('Servers/' . $i . '/auth_type') === 'cookie'); 145 $cookieAuthUsed |= $cookieAuthServer; 146 $serverName = $this->performConfigChecksServersGetServerName( 147 $this->cfg->getServerName($i), 148 $i 149 ); 150 $serverName = htmlspecialchars($serverName); 151 152 [$blowfishSecret, $blowfishSecretSet] 153 = $this->performConfigChecksServersSetBlowfishSecret( 154 $blowfishSecret, 155 $cookieAuthServer, 156 $blowfishSecretSet 157 ); 158 159 // $cfg['Servers'][$i]['ssl'] 160 // should be enabled if possible 161 if (! $this->cfg->getValue('Servers/' . $i . '/ssl')) { 162 $title = Descriptions::get('Servers/1/ssl') . ' (' . $serverName . ')'; 163 SetupIndex::messagesSet( 164 'notice', 165 'Servers/' . $i . '/ssl', 166 $title, 167 __( 168 'You should use SSL connections if your database server ' 169 . 'supports it.' 170 ) 171 ); 172 } 173 $sSecurityInfoMsg = Sanitize::sanitizeMessage(sprintf( 174 __( 175 'If you feel this is necessary, use additional protection settings - ' 176 . '%1$shost authentication%2$s settings and %3$strusted proxies list%4$s. ' 177 . 'However, IP-based protection may not be reliable if your IP belongs ' 178 . 'to an ISP where thousands of users, including you, are connected to.' 179 ), 180 '[a@' . Url::getCommon(['page' => 'servers', 'mode' => 'edit', 'id' => $i]) . '#tab_Server_config]', 181 '[/a]', 182 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', 183 '[/a]' 184 )); 185 186 // $cfg['Servers'][$i]['auth_type'] 187 // warn about full user credentials if 'auth_type' is 'config' 188 if ($this->cfg->getValue('Servers/' . $i . '/auth_type') === 'config' 189 && $this->cfg->getValue('Servers/' . $i . '/user') != '' 190 && $this->cfg->getValue('Servers/' . $i . '/password') != '' 191 ) { 192 $title = Descriptions::get('Servers/1/auth_type') 193 . ' (' . $serverName . ')'; 194 SetupIndex::messagesSet( 195 'notice', 196 'Servers/' . $i . '/auth_type', 197 $title, 198 Sanitize::sanitizeMessage(sprintf( 199 __( 200 'You set the [kbd]config[/kbd] authentication type and included ' 201 . 'username and password for auto-login, which is not a desirable ' 202 . 'option for live hosts. Anyone who knows or guesses your phpMyAdmin ' 203 . 'URL can directly access your phpMyAdmin panel. Set %1$sauthentication ' 204 . 'type%2$s to [kbd]cookie[/kbd] or [kbd]http[/kbd].' 205 ), 206 '[a@' . Url::getCommon(['page' => 'servers', 'mode' => 'edit', 'id' => $i]) . '#tab_Server]', 207 '[/a]' 208 )) 209 . ' ' . $sSecurityInfoMsg 210 ); 211 } 212 213 // $cfg['Servers'][$i]['AllowRoot'] 214 // $cfg['Servers'][$i]['AllowNoPassword'] 215 // serious security flaw 216 if (! $this->cfg->getValue('Servers/' . $i . '/AllowRoot') 217 || ! $this->cfg->getValue('Servers/' . $i . '/AllowNoPassword') 218 ) { 219 continue; 220 } 221 222 $title = Descriptions::get('Servers/1/AllowNoPassword') 223 . ' (' . $serverName . ')'; 224 SetupIndex::messagesSet( 225 'notice', 226 'Servers/' . $i . '/AllowNoPassword', 227 $title, 228 __('You allow for connecting to the server without a password.') 229 . ' ' . $sSecurityInfoMsg 230 ); 231 } 232 233 return [ 234 $cookieAuthUsed, 235 $blowfishSecret, 236 $blowfishSecretSet, 237 ]; 238 } 239 240 /** 241 * Set blowfish secret 242 * 243 * @param string|null $blowfishSecret Blowfish secret 244 * @param bool $cookieAuthServer Cookie auth is used 245 * @param bool $blowfishSecretSet Blowfish secret set 246 * 247 * @return array 248 */ 249 protected function performConfigChecksServersSetBlowfishSecret( 250 $blowfishSecret, 251 $cookieAuthServer, 252 $blowfishSecretSet 253 ): array { 254 if ($cookieAuthServer && $blowfishSecret === null) { 255 $blowfishSecretSet = true; 256 $this->cfg->set('blowfish_secret', Util::generateRandom(32)); 257 } 258 259 return [ 260 $blowfishSecret, 261 $blowfishSecretSet, 262 ]; 263 } 264 265 /** 266 * Define server name 267 * 268 * @param string $serverName Server name 269 * @param int $serverId Server id 270 * 271 * @return string Server name 272 */ 273 protected function performConfigChecksServersGetServerName( 274 $serverName, 275 $serverId 276 ) { 277 if ($serverName === 'localhost') { 278 return $serverName . ' [' . $serverId . ']'; 279 } 280 281 return $serverName; 282 } 283 284 /** 285 * Perform config checks for zip part. 286 * 287 * @return void 288 */ 289 protected function performConfigChecksZips() 290 { 291 $this->performConfigChecksServerGZipdump(); 292 $this->performConfigChecksServerBZipdump(); 293 $this->performConfigChecksServersZipdump(); 294 } 295 296 /** 297 * Perform config checks for zip part. 298 * 299 * @return void 300 */ 301 protected function performConfigChecksServersZipdump() 302 { 303 // $cfg['ZipDump'] 304 // requires zip_open in import 305 if ($this->cfg->getValue('ZipDump') && ! $this->functionExists('zip_open')) { 306 SetupIndex::messagesSet( 307 'error', 308 'ZipDump_import', 309 Descriptions::get('ZipDump'), 310 Sanitize::sanitizeMessage(sprintf( 311 __( 312 '%sZip decompression%s requires functions (%s) which are unavailable ' 313 . 'on this system.' 314 ), 315 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]', 316 '[/a]', 317 'zip_open' 318 )) 319 ); 320 } 321 322 // $cfg['ZipDump'] 323 // requires gzcompress in export 324 if (! $this->cfg->getValue('ZipDump') || $this->functionExists('gzcompress')) { 325 return; 326 } 327 328 SetupIndex::messagesSet( 329 'error', 330 'ZipDump_export', 331 Descriptions::get('ZipDump'), 332 Sanitize::sanitizeMessage(sprintf( 333 __( 334 '%sZip compression%s requires functions (%s) which are unavailable on ' 335 . 'this system.' 336 ), 337 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]', 338 '[/a]', 339 'gzcompress' 340 )) 341 ); 342 } 343 344 /** 345 * Check config of servers 346 * 347 * @param bool $cookieAuthUsed Cookie auth is used 348 * @param bool $blowfishSecretSet Blowfish secret set 349 * @param string $blowfishSecret Blowfish secret 350 * 351 * @return void 352 */ 353 protected function performConfigChecksCookieAuthUsed( 354 $cookieAuthUsed, 355 $blowfishSecretSet, 356 $blowfishSecret 357 ) { 358 // $cfg['blowfish_secret'] 359 // it's required for 'cookie' authentication 360 if (! $cookieAuthUsed) { 361 return; 362 } 363 364 if ($blowfishSecretSet) { 365 // 'cookie' auth used, blowfish_secret was generated 366 SetupIndex::messagesSet( 367 'notice', 368 'blowfish_secret_created', 369 Descriptions::get('blowfish_secret'), 370 Sanitize::sanitizeMessage(__( 371 'You didn\'t have blowfish secret set and have enabled ' 372 . '[kbd]cookie[/kbd] authentication, so a key was automatically ' 373 . 'generated for you. It is used to encrypt cookies; you don\'t need to ' 374 . 'remember it.' 375 )) 376 ); 377 } else { 378 $blowfishWarnings = []; 379 // check length 380 if (strlen($blowfishSecret) < 32) { 381 // too short key 382 $blowfishWarnings[] = __( 383 'Key is too short, it should have at least 32 characters.' 384 ); 385 } 386 // check used characters 387 $hasDigits = (bool) preg_match('/\d/', $blowfishSecret); 388 $hasChars = (bool) preg_match('/\S/', $blowfishSecret); 389 $hasNonword = (bool) preg_match('/\W/', $blowfishSecret); 390 if (! $hasDigits || ! $hasChars || ! $hasNonword) { 391 $blowfishWarnings[] = Sanitize::sanitizeMessage( 392 __( 393 'Key should contain letters, numbers [em]and[/em] ' 394 . 'special characters.' 395 ) 396 ); 397 } 398 if (! empty($blowfishWarnings)) { 399 SetupIndex::messagesSet( 400 'error', 401 'blowfish_warnings' . count($blowfishWarnings), 402 Descriptions::get('blowfish_secret'), 403 implode('<br>', $blowfishWarnings) 404 ); 405 } 406 } 407 } 408 409 /** 410 * Check configuration for login cookie 411 * 412 * @return void 413 */ 414 protected function performConfigChecksLoginCookie() 415 { 416 // $cfg['LoginCookieValidity'] 417 // value greater than session.gc_maxlifetime will cause 418 // random session invalidation after that time 419 $loginCookieValidity = $this->cfg->getValue('LoginCookieValidity'); 420 if ($loginCookieValidity > ini_get('session.gc_maxlifetime') 421 ) { 422 SetupIndex::messagesSet( 423 'error', 424 'LoginCookieValidity', 425 Descriptions::get('LoginCookieValidity'), 426 Sanitize::sanitizeMessage(sprintf( 427 __( 428 '%1$sLogin cookie validity%2$s greater than %3$ssession.gc_maxlifetime%4$s may ' 429 . 'cause random session invalidation (currently session.gc_maxlifetime ' 430 . 'is %5$d).' 431 ), 432 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', 433 '[/a]', 434 '[a@' . Core::getPHPDocLink('session.configuration.php#ini.session.gc-maxlifetime') . ']', 435 '[/a]', 436 ini_get('session.gc_maxlifetime') 437 )) 438 ); 439 } 440 441 // $cfg['LoginCookieValidity'] 442 // should be at most 1800 (30 min) 443 if ($loginCookieValidity > 1800) { 444 SetupIndex::messagesSet( 445 'notice', 446 'LoginCookieValidity', 447 Descriptions::get('LoginCookieValidity'), 448 Sanitize::sanitizeMessage(sprintf( 449 __( 450 '%sLogin cookie validity%s should be set to 1800 seconds (30 minutes) ' 451 . 'at most. Values larger than 1800 may pose a security risk such as ' 452 . 'impersonation.' 453 ), 454 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', 455 '[/a]' 456 )) 457 ); 458 } 459 460 // $cfg['LoginCookieValidity'] 461 // $cfg['LoginCookieStore'] 462 // LoginCookieValidity must be less or equal to LoginCookieStore 463 if (($this->cfg->getValue('LoginCookieStore') == 0) 464 || ($loginCookieValidity <= $this->cfg->getValue('LoginCookieStore')) 465 ) { 466 return; 467 } 468 469 SetupIndex::messagesSet( 470 'error', 471 'LoginCookieValidity', 472 Descriptions::get('LoginCookieValidity'), 473 Sanitize::sanitizeMessage(sprintf( 474 __( 475 'If using [kbd]cookie[/kbd] authentication and %sLogin cookie store%s ' 476 . 'is not 0, %sLogin cookie validity%s must be set to a value less or ' 477 . 'equal to it.' 478 ), 479 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', 480 '[/a]', 481 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', 482 '[/a]' 483 )) 484 ); 485 } 486 487 /** 488 * Check GZipDump configuration 489 * 490 * @return void 491 */ 492 protected function performConfigChecksServerBZipdump() 493 { 494 // $cfg['BZipDump'] 495 // requires bzip2 functions 496 if (! $this->cfg->getValue('BZipDump') 497 || ($this->functionExists('bzopen') && $this->functionExists('bzcompress')) 498 ) { 499 return; 500 } 501 502 $functions = $this->functionExists('bzopen') 503 ? '' : 504 'bzopen'; 505 $functions .= $this->functionExists('bzcompress') 506 ? '' 507 : ($functions ? ', ' : '') . 'bzcompress'; 508 SetupIndex::messagesSet( 509 'error', 510 'BZipDump', 511 Descriptions::get('BZipDump'), 512 Sanitize::sanitizeMessage( 513 sprintf( 514 __( 515 '%1$sBzip2 compression and decompression%2$s requires functions (%3$s) which ' 516 . 'are unavailable on this system.' 517 ), 518 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]', 519 '[/a]', 520 $functions 521 ) 522 ) 523 ); 524 } 525 526 /** 527 * Check GZipDump configuration 528 * 529 * @return void 530 */ 531 protected function performConfigChecksServerGZipdump() 532 { 533 // $cfg['GZipDump'] 534 // requires zlib functions 535 if (! $this->cfg->getValue('GZipDump') 536 || ($this->functionExists('gzopen') && $this->functionExists('gzencode')) 537 ) { 538 return; 539 } 540 541 SetupIndex::messagesSet( 542 'error', 543 'GZipDump', 544 Descriptions::get('GZipDump'), 545 Sanitize::sanitizeMessage(sprintf( 546 __( 547 '%1$sGZip compression and decompression%2$s requires functions (%3$s) which ' 548 . 'are unavailable on this system.' 549 ), 550 '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]', 551 '[/a]', 552 'gzencode' 553 )) 554 ); 555 } 556 557 /** 558 * Wrapper around function_exists to allow mock in test 559 * 560 * @param string $name Function name 561 * 562 * @return bool 563 */ 564 protected function functionExists($name) 565 { 566 return function_exists($name); 567 } 568} 569