‪TYPO3CMS  ‪main
SessionService.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of the TYPO3 CMS project.
5  *
6  * It is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU General Public License, either version 2
8  * of the License, or any later version.
9  *
10  * For the full copyright and license information, please read the
11  * LICENSE.txt file that was distributed with this source code.
12  *
13  * The TYPO3 project - inspiring people to share!
14  */
15 
17 
18 use Psr\Http\Message\ServerRequestInterface;
19 use Symfony\Component\HttpFoundation\Cookie;
37 
43 class SessionService implements SingletonInterface
44 {
45  use BlockSerializationTrait;
46 
52  private $cookieName = 'Typo3InstallTool';
53 
59  private $expireTimeInMinutes = 15;
60 
66  private $regenerateSessionIdTime = 5;
67 
68  public function __construct(private readonly HashService $hashService) {}
69 
70  public function installSessionHandler(): void
71  {
72  // Register our "save" session handler
73  $sessionHandler = GeneralUtility::makeInstance(
74  FileSessionHandler::class,
75  ‪Environment::getVarPath() . '/session',
76  $this->expireTimeInMinutes,
77  $this->hashService
78  );
79  session_set_save_handler($sessionHandler);
80  session_name($this->cookieName);
81  ini_set('session.cookie_secure', GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'On' : 'Off');
82  ini_set('session.cookie_httponly', 'On');
83  ini_set('session.cookie_samesite', Cookie::SAMESITE_STRICT);
84  ini_set('session.cookie_path', (string)GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'));
85  // Always call the garbage collector to clean up stale session files
86  ini_set('session.gc_probability', (string)100);
87  ini_set('session.gc_divisor', (string)100);
88  ini_set('session.gc_maxlifetime', (string)($this->expireTimeInMinutes * 2 * 60));
89  if ($this->isSessionAutoStartEnabled()) {
90  $sessionCreationError = 'Error: session.auto-start is enabled.<br />';
91  $sessionCreationError .= 'The PHP option session.auto-start is enabled. Disable this option in php.ini or .htaccess:<br />';
92  $sessionCreationError .= '<pre>php_value session.auto_start Off</pre>';
93  throw new Exception($sessionCreationError, 1294587485);
94  }
95  if (session_status() === PHP_SESSION_ACTIVE) {
96  $sessionCreationError = 'Session already started by session_start().<br />';
97  $sessionCreationError .= 'Make sure no installed extension is starting a session in its ext_localconf.php or ext_tables.php.';
98  throw new Exception($sessionCreationError, 1294587486);
99  }
100  }
101 
102  public function initializeSession()
103  {
104  if (session_status() === PHP_SESSION_ACTIVE) {
105  return;
106  }
107  session_start();
108  }
109 
115  public function startSession()
116  {
117  $this->initializeSession();
118  // check if session is already active
119  if ($_SESSION['active'] ?? false) {
120  return session_id();
121  }
122  $_SESSION['active'] = true;
123  // Be sure to use our own session id, so create a new one
124  return $this->renewSession();
125  }
126 
130  public function destroySession(?ServerRequestInterface $request)
131  {
132  $request = $request ?? ‪ServerRequestFactory::fromGlobals();
133  if ($this->hasSessionCookie($request)) {
134  $this->initializeSession();
135  $_SESSION = [];
136  $params = session_get_cookie_params();
137  $cookie = Cookie::create(($sessionName = session_name()) !== false ? $sessionName : $this->cookieName)
138  ->withValue('0')
139  ->withPath($params['path'])
140  ->withDomain($params['domain'])
141  ->withSecure($params['samesite'] === Cookie::SAMESITE_NONE || GeneralUtility::getIndpEnv('TYPO3_SSL'))
142  ->withHttpOnly($params['httponly'])
143  ->withSameSite($params['samesite']);
144 
145  header('Set-Cookie: ' . $cookie);
146  session_destroy();
147  }
148  }
149 
153  public function resetSession()
154  {
155  $this->initializeSession();
156  $_SESSION = [];
157  $_SESSION['active'] = false;
158  }
159 
165  private function renewSession()
166  {
167  // we do not have parallel ajax requests so we can safely remove the old session data
168  session_regenerate_id(true);
169  return session_id();
170  }
171 
175  public function hasSessionCookie(ServerRequestInterface $request): bool
176  {
177  return isset($request->getCookieParams()[$this->cookieName]);
178  }
179 
186  public function setAuthorized()
187  {
188  $_SESSION['authorized'] = true;
189  $_SESSION['lastSessionId'] = time();
190  $_SESSION['tstamp'] = time();
191  $_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
192  // Renew the session id to avoid session fixation
193  $this->renewSession();
194  }
195 
202  public function setAuthorizedBackendSession(UserSession $userSession)
203  {
204  $nonce = bin2hex(random_bytes(20));
205  $sessionBackend = $this->getBackendUserSessionBackend();
206  // use hash mechanism of session backend, or pass plain value through generic hmac
207  $sessionHmac = $sessionBackend instanceof HashableSessionBackendInterface
208  ? $sessionBackend->hash($userSession->getIdentifier())
209  : hash_hmac('sha256', $userSession->getIdentifier(), $nonce);
210 
211  $_SESSION['authorized'] = true;
212  $_SESSION['lastSessionId'] = time();
213  $_SESSION['tstamp'] = time();
214  $_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
215  $_SESSION['isBackendSession'] = true;
216  $_SESSION['backendUserSession'] = [
217  'nonce' => $nonce,
218  'userId' => $userSession->getUserId(),
219  'hmac' => $sessionHmac,
220  ];
221  // Renew the session id to avoid session fixation
222  $this->renewSession();
223  }
224 
230  public function isAuthorized(ServerRequestInterface $request)
231  {
232  if (!$this->hasSessionCookie($request)) {
233  return false;
234  }
235  $this->initializeSession();
236  if (empty($_SESSION['authorized'])) {
237  return false;
238  }
239  return !$this->isExpired($request);
240  }
241 
247  public function isAuthorizedBackendUserSession(ServerRequestInterface $request): bool
248  {
249  if (!$this->hasSessionCookie($request)) {
250  return false;
251  }
252  $this->initializeSession();
253  if (empty($_SESSION['authorized']) || empty($_SESSION['isBackendSession'])) {
254  return false;
255  }
256  return !$this->isExpired($request);
257  }
258 
265  public function hasActiveBackendUserRoleAndSession(): bool
266  {
267  // @see \TYPO3\CMS\Install\Controller\BackendModuleController::setAuthorizedAndRedirect()
268  $backendUserSession = $this->getBackendUserSession();
269  $backendUserRecord = $this->getBackendUserRecord($backendUserSession['userId']);
270  if ($backendUserRecord === null || empty($backendUserRecord['uid'])) {
271  return false;
272  }
273  $isAdmin = (($backendUserRecord['admin'] ?? 0) & 1) === 1;
274  $systemMaintainers = array_map('intval', ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? []);
275  // in case no system maintainers are configured, all admin users are considered to be system maintainers
276  $isSystemMaintainer = empty($systemMaintainers) || in_array((int)$backendUserRecord['uid'], $systemMaintainers, true);
277  // in development context, all admin users are considered to be system maintainers
278  $hasDevelopmentContext = ‪Environment::getContext()->isDevelopment();
279  // stop here, in case the current admin tool session does not belong to a backend user having admin & maintainer privileges
280  if (!$isAdmin || !$hasDevelopmentContext && !$isSystemMaintainer) {
281  return false;
282  }
283 
284  $sessionBackend = $this->getBackendUserSessionBackend();
285  foreach ($sessionBackend->getAll() as $sessionRecord) {
286  $sessionUserId = (int)($sessionRecord['ses_userid'] ?? 0);
287  // skip, in case backend user id does not match
288  if ($backendUserSession['userId'] !== $sessionUserId) {
289  continue;
290  }
291  $sessionId = (string)($sessionRecord['ses_id'] ?? '');
292  // use persisted hashed `ses_id` directly, or pass through hmac for plain values
293  $sessionHmac = $sessionBackend instanceof HashableSessionBackendInterface
294  ? $sessionId
295  : hash_hmac('sha256', $sessionId, $backendUserSession['nonce']);
296  // skip, in case backend user session id does not match
297  if ($backendUserSession['hmac'] !== $sessionHmac) {
298  continue;
299  }
300  // backend user id and session id matched correctly
301  return true;
302  }
303  return false;
304  }
305 
313  public function isExpired(ServerRequestInterface $request)
314  {
315  if (!$this->hasSessionCookie($request)) {
316  // Session never existed, means it is not "expired"
317  return false;
318  }
319  $this->initializeSession();
320  if (empty($_SESSION['authorized'])) {
321  // Session never authorized, means it is not "expired"
322  return false;
323  }
324  return $_SESSION['expires'] <= time();
325  }
326 
332  public function refreshSession()
333  {
334  $_SESSION['tstamp'] = time();
335  $_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
336  if (time() > $_SESSION['lastSessionId'] + $this->regenerateSessionIdTime * 60) {
337  // Renew our session ID
338  $_SESSION['lastSessionId'] = time();
339  $this->renewSession();
340  }
341  }
342 
348  public function addMessage(FlashMessage $message)
349  {
350  if (!is_array($_SESSION['messages'])) {
351  $_SESSION['messages'] = [];
352  }
353  $_SESSION['messages'][] = $message;
354  }
355 
361  public function getMessagesAndFlush()
362  {
363  $messages = [];
364  if (is_array($_SESSION['messages'])) {
365  $messages = $_SESSION['messages'];
366  }
367  $_SESSION['messages'] = [];
368  return $messages;
369  }
370 
374  public function getBackendUserSession(): array
375  {
376  if (empty($_SESSION['backendUserSession'])) {
377  throw new Exception(
378  'The backend user session is only available if invoked via the backend user interface.',
379  1624879295
380  );
381  }
382  return $_SESSION['backendUserSession'];
383  }
384 
390  protected function isSessionAutoStartEnabled()
391  {
392  return $this->getIniValueBoolean('session.auto_start');
393  }
394 
401  protected function getIniValueBoolean($configOption)
402  {
403  return filter_var(
404  ini_get($configOption),
405  FILTER_VALIDATE_BOOLEAN,
406  [FILTER_REQUIRE_SCALAR, FILTER_NULL_ON_FAILURE]
407  );
408  }
409 
417  protected function getBackendUserRecord(int ‪$uid): ?array
418  {
419  $restrictionContainer = GeneralUtility::makeInstance(DefaultRestrictionContainer::class);
420  $restrictionContainer->add(GeneralUtility::makeInstance(RootLevelRestriction::class, ['be_users']));
421 
422  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
423  $queryBuilder->setRestrictions($restrictionContainer);
424  $queryBuilder->select('uid', 'admin')
425  ->from('be_users')
426  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)));
427 
428  $resetBeUsersTca = false;
429  if (!isset(‪$GLOBALS['TCA']['be_users'])) {
430  // The admin tool intentionally does not load any TCA information at this time.
431  // The database restictions, needs the enablecolumns TCA information
432  // for 'be_users' to load the user correctly.
433  // That is why this part of the TCA ($GLOBALS['TCA']['be_users']['ctrl']['enablecolumns'])
434  // is simulated.
435  // The simulation state will be removed later to avoid unexpected side effects.
436  ‪$GLOBALS['TCA']['be_users']['ctrl']['enablecolumns'] = [
437  'rootLevel' => 1,
438  'deleted' => 'deleted',
439  'disabled' => 'disable',
440  'starttime' => 'starttime',
441  'endtime' => 'endtime',
442  ];
443  $resetBeUsersTca = true;
444  }
445  $result = $queryBuilder->executeQuery()->fetchAssociative();
446  if ($resetBeUsersTca) {
447  unset(‪$GLOBALS['TCA']['be_users']);
448  }
449 
450  return is_array($result) ? $result : null;
451  }
452 
453  protected function getBackendUserSessionBackend(): SessionBackendInterface
454  {
455  return GeneralUtility::makeInstance(SessionManager::class)->getSessionBackend('BE');
456  }
457 }
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:52
‪TYPO3\CMS\Core\Session\UserSession
Definition: UserSession.php:45
‪TYPO3\CMS\Install\Service\Session\FileSessionHandler
Definition: FileSessionHandler.php:30
‪TYPO3\CMS\Core\Session\SessionManager
Definition: SessionManager.php:41
‪TYPO3\CMS\Core\Session\Backend\HashableSessionBackendInterface
Definition: HashableSessionBackendInterface.php:21
‪TYPO3\CMS\Core\Core\Environment\getVarPath
‪static getVarPath()
Definition: Environment.php:197
‪TYPO3\CMS\Core\Security\BlockSerializationTrait
Definition: BlockSerializationTrait.php:28
‪TYPO3\CMS\Install\Exception
Definition: Exception.php:23
‪TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction
Definition: RootLevelRestriction.php:27
‪TYPO3\CMS\Core\Session\Backend\SessionBackendInterface
Definition: SessionBackendInterface.php:28
‪TYPO3\CMS\Core\Http\ServerRequestFactory
Definition: ServerRequestFactory.php:35
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:41
‪TYPO3\CMS\Core\Messaging\FlashMessage
Definition: FlashMessage.php:27
‪TYPO3\CMS\Webhooks\Message\$uid
‪identifier readonly int $uid
Definition: PageModificationMessage.php:35
‪TYPO3\CMS\Core\SingletonInterface
Definition: SingletonInterface.php:22
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:41
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Crypto\HashService
Definition: HashService.php:27
‪TYPO3\CMS\Install\Service
Definition: ClearCacheService.php:16
‪TYPO3\CMS\Core\Core\Environment\getContext
‪static getContext()
Definition: Environment.php:128
‪TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer
Definition: DefaultRestrictionContainer.php:24
‪TYPO3\CMS\Core\Http\ServerRequestFactory\fromGlobals
‪static ServerRequest fromGlobals()
Definition: ServerRequestFactory.php:59