ServerConfigChecks.php 20 KB

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