Session.php 7.8 KB

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