Session.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. <?php
  2. /* vim: set expandtab sw=4 ts=4 sts=4: */
  3. /**
  4. * Session handling
  5. *
  6. * @package PhpMyAdmin
  7. *
  8. * @see https://www.php.net/manual/en/features.sessions.php
  9. */
  10. declare(strict_types=1);
  11. namespace PhpMyAdmin;
  12. use PhpMyAdmin\Config;
  13. use PhpMyAdmin\Core;
  14. use PhpMyAdmin\ErrorHandler;
  15. use PhpMyAdmin\Util;
  16. /**
  17. * Session class
  18. *
  19. * @package PhpMyAdmin
  20. */
  21. class Session
  22. {
  23. /**
  24. * Generates PMA_token session variable.
  25. *
  26. * @return void
  27. */
  28. private static function generateToken()
  29. {
  30. $_SESSION[' PMA_token '] = Util::generateRandom(16, true);
  31. $_SESSION[' HMAC_secret '] = Util::generateRandom(16);
  32. /**
  33. * Check if token is properly generated (the generation can fail, for example
  34. * due to missing /dev/random for openssl).
  35. */
  36. if (empty($_SESSION[' PMA_token '])) {
  37. Core::fatalError(
  38. 'Failed to generate random CSRF token!'
  39. );
  40. }
  41. }
  42. /**
  43. * tries to secure session from hijacking and fixation
  44. * should be called before login and after successful login
  45. * (only required if sensitive information stored in session)
  46. *
  47. * @return void
  48. */
  49. public static function secure()
  50. {
  51. // prevent session fixation and XSS
  52. if (session_status() === PHP_SESSION_ACTIVE && ! defined('TESTSUITE')) {
  53. session_regenerate_id(true);
  54. }
  55. // continue with empty session
  56. session_unset();
  57. self::generateToken();
  58. }
  59. /**
  60. * Session failed function
  61. *
  62. * @param array $errors PhpMyAdmin\ErrorHandler array
  63. *
  64. * @return void
  65. */
  66. private static function sessionFailed(array $errors)
  67. {
  68. $messages = [];
  69. foreach ($errors as $error) {
  70. /*
  71. * Remove path from open() in error message to avoid path disclossure
  72. *
  73. * This can happen with PHP 5 when nonexisting session ID is provided,
  74. * since PHP 7, session existence is checked first.
  75. *
  76. * This error can also happen in case of session backed error (eg.
  77. * read only filesystem) on any PHP version.
  78. *
  79. * The message string is currently hardcoded in PHP, so hopefully it
  80. * will not change in future.
  81. */
  82. $messages[] = preg_replace(
  83. '/open\(.*, O_RDWR\)/',
  84. 'open(SESSION_FILE, O_RDWR)',
  85. htmlspecialchars($error->getMessage())
  86. );
  87. }
  88. /*
  89. * Session initialization is done before selecting language, so we
  90. * can not use translations here.
  91. */
  92. Core::fatalError(
  93. 'Error during session start; please check your PHP and/or '
  94. . 'webserver log file and configure your PHP '
  95. . 'installation properly. Also ensure that cookies are enabled '
  96. . 'in your browser.'
  97. . '<br><br>'
  98. . implode('<br><br>', $messages)
  99. );
  100. }
  101. /**
  102. * Set up session
  103. *
  104. * @param Config $config Configuration handler
  105. * @param ErrorHandler $errorHandler Error handler
  106. * @return void
  107. */
  108. public static function setUp(Config $config, ErrorHandler $errorHandler)
  109. {
  110. // verify if PHP supports session, die if it does not
  111. if (! function_exists('session_name')) {
  112. Core::warnMissingExtension('session', true);
  113. } elseif (! empty(ini_get('session.auto_start'))
  114. && session_name() != 'phpMyAdmin'
  115. && ! empty(session_id())) {
  116. // Do not delete the existing non empty session, it might be used by
  117. // other applications; instead just close it.
  118. if (empty($_SESSION)) {
  119. // Ignore errors as this might have been destroyed in other
  120. // request meanwhile
  121. @session_destroy();
  122. } elseif (function_exists('session_abort')) {
  123. // PHP 5.6 and newer
  124. session_abort();
  125. } else {
  126. session_write_close();
  127. }
  128. }
  129. // session cookie settings
  130. session_set_cookie_params(
  131. 0,
  132. $config->getRootPath(),
  133. '',
  134. $config->isHttps(),
  135. true
  136. );
  137. // cookies are safer (use ini_set() in case this function is disabled)
  138. ini_set('session.use_cookies', 'true');
  139. // optionally set session_save_path
  140. $path = $config->get('SessionSavePath');
  141. if (! empty($path)) {
  142. session_save_path($path);
  143. // We can not do this unconditionally as this would break
  144. // any more complex setup (eg. cluster), see
  145. // https://github.com/phpmyadmin/phpmyadmin/issues/8346
  146. ini_set('session.save_handler', 'files');
  147. }
  148. // use cookies only
  149. ini_set('session.use_only_cookies', '1');
  150. // strict session mode (do not accept random string as session ID)
  151. ini_set('session.use_strict_mode', '1');
  152. // make the session cookie HttpOnly
  153. ini_set('session.cookie_httponly', '1');
  154. // do not force transparent session ids
  155. ini_set('session.use_trans_sid', '0');
  156. // delete session/cookies when browser is closed
  157. ini_set('session.cookie_lifetime', '0');
  158. // some pages (e.g. stylesheet) may be cached on clients, but not in shared
  159. // proxy servers
  160. session_cache_limiter('private');
  161. $httpCookieName = $config->getCookieName('phpMyAdmin');
  162. @session_name($httpCookieName);
  163. // Restore correct sesion ID (it might have been reset by auto started session
  164. if ($config->issetCookie('phpMyAdmin')) {
  165. session_id($config->getCookie('phpMyAdmin'));
  166. }
  167. // on first start of session we check for errors
  168. // f.e. session dir cannot be accessed - session file not created
  169. $orig_error_count = $errorHandler->countErrors(false);
  170. $session_result = session_start();
  171. if ($session_result !== true
  172. || $orig_error_count != $errorHandler->countErrors(false)
  173. ) {
  174. setcookie($httpCookieName, '', 1);
  175. $errors = $errorHandler->sliceErrors($orig_error_count);
  176. self::sessionFailed($errors);
  177. }
  178. unset($orig_error_count, $session_result);
  179. /**
  180. * Disable setting of session cookies for further session_start() calls.
  181. */
  182. if (session_status() !== PHP_SESSION_ACTIVE) {
  183. ini_set('session.use_cookies', 'true');
  184. }
  185. /**
  186. * Token which is used for authenticating access queries.
  187. * (we use "space PMA_token space" to prevent overwriting)
  188. */
  189. if (empty($_SESSION[' PMA_token '])) {
  190. self::generateToken();
  191. /**
  192. * Check for disk space on session storage by trying to write it.
  193. *
  194. * This seems to be most reliable approach to test if sessions are working,
  195. * otherwise the check would fail with custom session backends.
  196. */
  197. $orig_error_count = $errorHandler->countErrors();
  198. session_write_close();
  199. if ($errorHandler->countErrors() > $orig_error_count) {
  200. $errors = $errorHandler->sliceErrors($orig_error_count);
  201. self::sessionFailed($errors);
  202. }
  203. session_start();
  204. if (empty($_SESSION[' PMA_token '])) {
  205. Core::fatalError(
  206. 'Failed to store CSRF token in session! ' .
  207. 'Probably sessions are not working properly.'
  208. );
  209. }
  210. }
  211. }
  212. }