TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,460 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Install\Service;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Http\NormalizedParams;
|
||||
use TYPO3\CMS\Core\Http\ServerRequestFactory;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Security\BlockSerializationTrait;
|
||||
use TYPO3\CMS\Core\Session\Backend\HashableSessionBackendInterface;
|
||||
use TYPO3\CMS\Core\Session\Backend\SessionBackendInterface;
|
||||
use TYPO3\CMS\Core\Session\SessionManager;
|
||||
use TYPO3\CMS\Core\Session\UserSession;
|
||||
use TYPO3\CMS\Core\SingletonInterface;
|
||||
use TYPO3\CMS\Install\Exception;
|
||||
use TYPO3\CMS\Install\Service\Session\FileSessionHandler;
|
||||
|
||||
/**
|
||||
* Secure session handling for the install tool.
|
||||
*
|
||||
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
class SessionService implements SingletonInterface
|
||||
{
|
||||
use BlockSerializationTrait;
|
||||
|
||||
/**
|
||||
* the cookie to store the session ID of the install tool
|
||||
*/
|
||||
private string $cookieName = 'Typo3InstallTool';
|
||||
|
||||
/**
|
||||
* time (minutes) to expire an unused session
|
||||
*/
|
||||
private int $expireTimeInMinutes = 15;
|
||||
|
||||
/**
|
||||
* time (minutes) to generate a new session id for our current session
|
||||
*/
|
||||
private int $regenerateSessionIdTime = 5;
|
||||
|
||||
public function __construct(
|
||||
protected readonly LateBootService $lateBootService,
|
||||
protected readonly LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
public function installSessionHandler(?ServerRequestInterface $request): void
|
||||
{
|
||||
// Register our "save" session handler
|
||||
$sessionHandlerClass = $GLOBALS['TYPO3_CONF_VARS']['BE']['installToolSessionHandler']['className'] ?? FileSessionHandler::class;
|
||||
$options = $GLOBALS['TYPO3_CONF_VARS']['BE']['installToolSessionHandler']['options'] ?? [];
|
||||
$options['expirationTimeInMinutes'] = $this->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.<br />';
|
||||
$sessionCreationError .= 'The PHP option session.auto-start is enabled. Disable this option in php.ini or .htaccess:<br />';
|
||||
$sessionCreationError .= '<pre>php_value session.auto_start Off</pre>';
|
||||
throw new Exception($sessionCreationError, 1294587485);
|
||||
}
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
$sessionCreationError = 'Session already started by session_start().<br />';
|
||||
$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<string, int>|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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user