Session.php 8.0 KB

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