ServerConfigChecks.php 20 KB

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