expireTimeInMinutes; try { $sessionHandler = new $sessionHandlerClass(...$options); } catch (\Throwable $throwable) { $this->logger->error('Session handler is not configured properly: ' . $throwable->getMessage()); // Regardless of ANY misconfiguration, we expect the session handler - like the whole install tool - to work // at ANY time. For this reason, any PHP error or misconfiguration fails silently to the FileSessionHandler. $sessionHandler = $this->getDefaultSessionHandler(); } $request = $request ?? ServerRequestFactory::fromGlobals(); $normalizedParams = $request->getAttribute('normalizedParams') ?? NormalizedParams::createFromRequest($request); session_set_save_handler($sessionHandler); session_name($this->cookieName); ini_set('session.cookie_secure', $normalizedParams->isHttps() ? 'On' : 'Off'); ini_set('session.cookie_httponly', 'On'); ini_set('session.cookie_samesite', Cookie::SAMESITE_STRICT); ini_set('session.cookie_path', $normalizedParams->getSitePath()); // Always call the garbage collector to clean up stale session files ini_set('session.gc_probability', (string)100); ini_set('session.gc_divisor', (string)100); ini_set('session.gc_maxlifetime', (string)($this->expireTimeInMinutes * 2 * 60)); if ($this->isSessionAutoStartEnabled()) { $sessionCreationError = 'Error: session.auto-start is enabled.
'; $sessionCreationError .= 'The PHP option session.auto-start is enabled. Disable this option in php.ini or .htaccess:
'; $sessionCreationError .= '
php_value session.auto_start Off
'; throw new Exception($sessionCreationError, 1294587485); } if (session_status() === PHP_SESSION_ACTIVE) { $sessionCreationError = 'Session already started by session_start().
'; $sessionCreationError .= 'Make sure no installed extension is starting a session in its ext_localconf.php.'; throw new Exception($sessionCreationError, 1294587486); } } protected function getDefaultSessionHandler(): \SessionHandlerInterface { return new FileSessionHandler($this->expireTimeInMinutes); } public function initializeSession() { if (session_status() === PHP_SESSION_ACTIVE) { return; } session_start(); } /** * Starts a new session * * @return string|false The session ID */ public function startSession() { $this->initializeSession(); // check if session is already active if ($_SESSION['active'] ?? false) { return session_id(); } $_SESSION['active'] = true; // Be sure to use our own session id, so create a new one return $this->renewSession(); } /** * Destroys a session */ public function destroySession(?ServerRequestInterface $request): void { $request = $request ?? ServerRequestFactory::fromGlobals(); if ($this->hasSessionCookie($request)) { $normalizedParams = $request->getAttribute('normalizedParams') ?? NormalizedParams::createFromRequest($request); $this->initializeSession(); $_SESSION = []; $params = session_get_cookie_params(); $cookie = Cookie::create(($sessionName = session_name()) !== false ? $sessionName : $this->cookieName) ->withValue('0') ->withPath($params['path']) ->withDomain($params['domain']) ->withSecure($params['samesite'] === Cookie::SAMESITE_NONE || $normalizedParams->isHttps()) ->withHttpOnly($params['httponly']) ->withSameSite($params['samesite']); header('Set-Cookie: ' . $cookie); session_destroy(); } } /** * Reset session. Sets _SESSION to empty array. */ public function resetSession() { $this->initializeSession(); $_SESSION = []; $_SESSION['active'] = false; } /** * Generates a new session ID and sends it to the client. * * @return string|false the new session ID */ private function renewSession() { // we do not have parallel ajax requests, so we can safely remove the old session data session_regenerate_id(true); return session_id(); } /** * Checks whether is session cookie is set */ public function hasSessionCookie(ServerRequestInterface $request): bool { return isset($request->getCookieParams()[$this->cookieName]); } /** * Marks this session as an "authorized" one (login successful). * Should only be called if: * a) we have a valid session running * b) the "password" or some other authorization mechanism really matched */ public function setAuthorized() { $_SESSION['authorized'] = true; $_SESSION['lastSessionId'] = time(); $_SESSION['tstamp'] = time(); $_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60; // Renew the session id to avoid session fixation $this->renewSession(); } /** * Marks this session as an "authorized by backend user" one. * This is called by BackendModuleController from backend context. * * @param UserSession $userSession session of the current backend user */ public function setAuthorizedBackendSession(UserSession $userSession, SessionBackendInterface $sessionBackend) { $nonce = bin2hex(random_bytes(20)); // use hash mechanism of session backend, or pass plain value through generic hmac $sessionHmac = $sessionBackend instanceof HashableSessionBackendInterface ? $sessionBackend->hash($userSession->getIdentifier()) : hash_hmac('sha256', $userSession->getIdentifier(), $nonce); $_SESSION['authorized'] = true; $_SESSION['lastSessionId'] = time(); $_SESSION['tstamp'] = time(); $_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60; $_SESSION['isBackendSession'] = true; $_SESSION['backendUserSession'] = [ 'nonce' => $nonce, 'userId' => $userSession->getUserId(), 'hmac' => $sessionHmac, ]; // Renew the session id to avoid session fixation $this->renewSession(); } /** * Check if we have an already authorized session * * @return bool TRUE if this session has been authorized before (by a correct password) */ public function isAuthorized(ServerRequestInterface $request): bool { if (!$this->hasSessionCookie($request)) { return false; } $this->initializeSession(); if (empty($_SESSION['authorized'])) { return false; } return !$this->isExpired($request); } /** * Check if we have an authorized session from a system maintainer * * @return bool TRUE if this session has been authorized before and initialized by a backend system maintainer */ public function isAuthorizedBackendUserSession(ServerRequestInterface $request): bool { if (!$this->hasSessionCookie($request)) { return false; } $this->initializeSession(); if (empty($_SESSION['authorized']) || empty($_SESSION['isBackendSession'])) { return false; } return !$this->isExpired($request); } /** * Evaluates whether the backend user that initiated this admin tool session, * has an active role (is still admin & system maintainer) and has an active backend user interface session. * * @return bool whether the backend user has an active role and backend user interface session */ public function hasActiveBackendUserRoleAndSession(): bool { $container = $this->lateBootService->getContainer( // Allow DI caching because this request was forwarded from a backend session, // and therefore failsafe requirements do not apply true ); // Unset internal container instance in order for later services // to be able to bootstrap a fresh container $this->lateBootService->unsetInternalContainerInstance(); // @see \TYPO3\CMS\Install\Controller\BackendModuleController::setAuthorizedAndRedirect() $backendUserSession = $this->getBackendUserSession(); $backendUserRecord = $this->getBackendUserRecord($container, $backendUserSession['userId']); if ($backendUserRecord === null || empty($backendUserRecord['uid'])) { return false; } $isAdmin = (($backendUserRecord['admin'] ?? 0) & 1) === 1; $systemMaintainers = array_map('intval', $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? []); // in case no system maintainers are configured, all admin users are considered to be system maintainers $isSystemMaintainer = empty($systemMaintainers) || in_array((int)$backendUserRecord['uid'], $systemMaintainers, true); // in development context, all admin users are considered to be system maintainers $hasDevelopmentContext = Environment::getContext()->isDevelopment(); // stop here, in case the current admin tool session does not belong to a backend user having admin & maintainer privileges if (!$isAdmin || !$hasDevelopmentContext && !$isSystemMaintainer) { return false; } $sessionBackend = $container->get(SessionManager::class)->getSessionBackend('BE'); foreach ($sessionBackend->getAll() as $sessionRecord) { $sessionUserId = (int)($sessionRecord['ses_userid'] ?? 0); // skip, in case backend user id does not match if ($backendUserSession['userId'] !== $sessionUserId) { continue; } $sessionId = (string)($sessionRecord['ses_id'] ?? ''); // use persisted hashed `ses_id` directly, or pass through hmac for plain values $sessionHmac = $sessionBackend instanceof HashableSessionBackendInterface ? $sessionId : hash_hmac('sha256', $sessionId, $backendUserSession['nonce']); // skip, in case backend user session id does not match if ($backendUserSession['hmac'] !== $sessionHmac) { continue; } // backend user id and session id matched correctly return true; } return false; } /** * Check if our session is expired. * Useful only right after a FALSE "isAuthorized" to see if this is the * reason for not being authorized anymore. * * @return bool TRUE if an authorized session exists, but is expired */ public function isExpired(ServerRequestInterface $request) { if (!$this->hasSessionCookie($request)) { // Session never existed, means it is not "expired" return false; } $this->initializeSession(); if (empty($_SESSION['authorized'])) { // Session never authorized, means it is not "expired" return false; } return $_SESSION['expires'] <= time(); } /** * Refreshes our session information, rising the expire time. * Also generates a new session ID every 5 minutes to minimize the risk of * session hijacking. */ public function refreshSession() { $_SESSION['tstamp'] = time(); $_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60; if (time() > $_SESSION['lastSessionId'] + $this->regenerateSessionIdTime * 60) { // Renew our session ID $_SESSION['lastSessionId'] = time(); $this->renewSession(); } } /** * Add a message to "Flash" message storage. * * @param FlashMessage $message A message to add */ public function addMessage(FlashMessage $message) { if (!is_array($_SESSION['messages'])) { $_SESSION['messages'] = []; } $_SESSION['messages'][] = $message; } /** * Return stored session messages and flush. * * @return FlashMessage[] Messages */ public function getMessagesAndFlush() { $messages = []; if (is_array($_SESSION['messages'])) { $messages = $_SESSION['messages']; } $_SESSION['messages'] = []; return $messages; } /** * @return array{userId: int, nonce: string, hmac: string} backend user session references */ public function getBackendUserSession(): array { if (empty($_SESSION['backendUserSession'])) { throw new Exception( 'The backend user session is only available if invoked via the backend user interface.', 1624879295 ); } return $_SESSION['backendUserSession']; } /** * Check if php session.auto_start is enabled * * @return bool TRUE if session.auto_start is enabled, FALSE if disabled */ protected function isSessionAutoStartEnabled() { return $this->getIniValueBoolean('session.auto_start'); } /** * Cast an on/off php ini value to boolean * * @param string $configOption * @return bool TRUE if the given option is enabled, FALSE if disabled */ protected function getIniValueBoolean($configOption) { return filter_var( ini_get($configOption), FILTER_VALIDATE_BOOLEAN, [FILTER_REQUIRE_SCALAR, FILTER_NULL_ON_FAILURE] ); } /** * Fetching a user record with uid=$uid. * Functionally similar to TYPO3\CMS\Core\Authentication\BackendUserAuthentication::setBeUserByUid(). * * @param int $uid The UID of the backend user * @return array|null The backend user record or NULL */ protected function getBackendUserRecord(ContainerInterface $container, int $uid): ?array { $accessTimeStamp = (int)$GLOBALS['SIM_ACCESS_TIME']; $queryBuilder = $container->get(ConnectionPool::class)->getQueryBuilderForTable('be_users'); $queryBuilder->select('uid', 'admin') ->from('be_users') ->where( $queryBuilder->expr()->and( $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)), // The admin tool intentionally does not load TCA schema at this time, // therefore database restrictions applied manually $queryBuilder->expr()->eq('pid', 0), $queryBuilder->expr()->eq('deleted', 0), $queryBuilder->expr()->eq('disable', 0), $queryBuilder->expr()->lte('starttime', $accessTimeStamp), $queryBuilder->expr()->or( $queryBuilder->expr()->eq('endtime', 0), $queryBuilder->expr()->gt('endtime', $accessTimeStamp), ) ) ); $result = $queryBuilder->executeQuery()->fetchAssociative(); return is_array($result) ? $result : null; } }