TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
@@ -0,0 +1,149 @@
<?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\Core\FormProtection;
use TYPO3\CMS\Core\Crypto\HashAlgo;
use TYPO3\CMS\Core\Crypto\HashService;
use TYPO3\CMS\Core\Crypto\Random;
use TYPO3\CMS\Core\Security\BlockSerializationTrait;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This class provides protection against cross-site request forgery (XSRF/CSRF)
* for forms.
*
* For documentation on how to use this class, please see the documentation of
* the corresponding subclasses
*/
abstract class AbstractFormProtection
{
use BlockSerializationTrait;
/**
* @var \Closure|null
*/
protected $validationFailedCallback;
/**
* The session token which is used to be hashed during token generation.
*
* @var string|null
*/
protected $sessionToken;
/**
* @return string
*/
protected function getSessionToken()
{
$this->sessionToken = $this->sessionToken ?? $this->retrieveSessionToken();
return $this->sessionToken;
}
/**
* Deletes the session token and persists the (empty) token.
*
* This function is intended to be called when a user logs on or off.
*/
public function clean()
{
$this->sessionToken = '';
$this->persistSessionToken();
}
/**
* Generates a token for a form by hashing the given parameters
* with the secret session token.
*
* Calling this function two times with the same parameters will create
* the same valid token during one user session.
*
* @param string $formName
* @param string $action
* @param string $formInstanceName
* @return string the 32-character hex ID of the generated token
* @throws \InvalidArgumentException
*/
public function generateToken($formName, $action = '', $formInstanceName = '')
{
if ($formName == '') {
throw new \InvalidArgumentException('$formName must not be empty.', 1294586643);
}
$hashService = GeneralUtility::makeInstance(HashService::class);
return $hashService->hmac($formName . $action . $formInstanceName . $this->getSessionToken(), self::class, HashAlgo::SHA3_256);
}
/**
* Checks whether the token $tokenId is valid in the form $formName with
* $formInstanceName.
*
* @param string $tokenId
* @param string $formName
* @param string $action
* @param string $formInstanceName
* @return bool
*/
public function validateToken($tokenId, $formName, $action = '', $formInstanceName = '')
{
$hashService = GeneralUtility::makeInstance(HashService::class);
$validTokenId = $hashService->hmac(((string)$formName . (string)$action) . (string)$formInstanceName . $this->getSessionToken(), self::class, HashAlgo::SHA3_256);
if (hash_equals($validTokenId, (string)$tokenId)) {
$isValid = true;
} else {
$isValid = false;
}
if (!$isValid) {
$this->createValidationErrorMessage();
}
return $isValid;
}
/**
* Generates the random token which is used in the hash for the form tokens.
*
* @return string
*/
protected function generateSessionToken()
{
return GeneralUtility::makeInstance(Random::class)->generateRandomHexString(64);
}
/**
* Creates or displays an error message telling the user that the submitted
* form token is invalid.
*/
protected function createValidationErrorMessage()
{
if ($this->validationFailedCallback !== null) {
$this->validationFailedCallback->__invoke();
}
}
/**
* Retrieves the session token.
*
* @return string
*/
abstract protected function retrieveSessionToken();
/**
* Saves the session token so that it can be used by a later incarnation
* of this class.
*
* @internal
*/
abstract public function persistSessionToken();
}
@@ -0,0 +1,180 @@
<?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\Core\FormProtection;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Error\Exception;
use TYPO3\CMS\Core\Registry;
/**
* This class provides protection against cross-site request forgery (XSRF/CSRF)
* for forms in the BE.
*
* How to use:
*
* For each form in the BE (or link that changes some data), create a token and
* insert is as a hidden form element. The name of the form element does not
* matter; you only need it to get the form token for verifying it.
*
* <pre>
* $formToken = GeneralUtility::makeInstance(FormProtectionFactory::class)->createFromType('backend')
* ->generateToken(
* 'BE user setup', 'edit'
* );
* $this->content .= '<input type="hidden" name="formToken" value="' .
* $formToken . '" />';
* </pre>
*
* The three parameters $formName, $action and $formInstanceName can be
* arbitrary strings, but they should make the form token as specific as
* possible. For different forms (e.g. BE user setup and editing a tt_content
* record) or different records (with different UIDs) from the same table,
* those values should be different.
*
* For editing a tt_content record, the call could look like this:
*
* <pre>
* $formToken = GeneralUtility::makeInstance(FormProtectionFactory::class)->createFromType('backend')
* ->getFormProtection()->generateToken(
* 'tt_content', 'edit', $uid
* );
* </pre>
*
*
* When processing the data that has been submitted by the form, you can check
* that the form token is valid like this:
*
* <pre>
* if ($dataHasBeenSubmitted && GeneralUtility::makeInstance(FormProtectionFactory::class)->createFromType('backend')
* ->validateToken(
* \TYPO3\CMS\Core\Utility\GeneralUtility::_POST('formToken'),
* 'BE user setup', 'edit
* )
* ) {
* processes the data
* } else {
* no need to do anything here as the BE form protection will create a
* flash message for an invalid token
* }
* </pre>
*/
class BackendFormProtection extends AbstractFormProtection
{
/**
* Keeps the instance of the user which existed during creation
* of the object.
*
* @var BackendUserAuthentication
*/
protected $backendUser;
/**
* Instance of the registry, which is used to permanently persist
* the session token so that it can be restored during re-login.
*
* @var Registry
*/
protected $registry;
/**
* Only allow construction if we have an authorized backend session
*
* @throws \TYPO3\CMS\Core\Error\Exception
*/
public function __construct(BackendUserAuthentication $backendUser, Registry $registry, ?\Closure $validationFailedCallback = null)
{
$this->backendUser = $backendUser;
$this->registry = $registry;
$this->validationFailedCallback = $validationFailedCallback;
if (!$this->isAuthorizedBackendSession()) {
throw new Exception('A back-end form protection may only be instantiated if there is an active back-end session.', 1285067843);
}
}
/**
* Retrieves the saved session token or generates a new one.
*
* @return string
*/
protected function retrieveSessionToken()
{
$this->sessionToken = $this->backendUser->getSessionData('formProtectionSessionToken');
if (empty($this->sessionToken)) {
$this->sessionToken = $this->generateSessionToken();
$this->persistSessionToken();
}
return $this->sessionToken;
}
/**
* Saves the tokens so that they can be used by a later incarnation of this
* class.
*
* @internal
*/
public function persistSessionToken()
{
$this->backendUser->setAndSaveSessionData('formProtectionSessionToken', $this->sessionToken);
}
/**
* Sets the session token for the user from the registry
* and returns it additionally.
*
* @internal
* @return string
* @throws \UnexpectedValueException
*/
public function setSessionTokenFromRegistry()
{
$this->sessionToken = $this->registry->get('core', 'formProtectionSessionToken:' . $this->backendUser->user['uid']);
if (empty($this->sessionToken)) {
throw new \UnexpectedValueException('Failed to restore the session token from the registry.', 1301827270);
}
return $this->sessionToken;
}
/**
* Stores the session token in the registry to have it
* available during re-login of the user.
*
* @internal
*/
public function storeSessionTokenInRegistry()
{
$this->registry->set('core', 'formProtectionSessionToken:' . $this->backendUser->user['uid'], $this->getSessionToken());
}
/**
* Removes the session token for the user from the registry.
*
* @internal
*/
public function removeSessionTokenFromRegistry()
{
$this->registry->remove('core', 'formProtectionSessionToken:' . $this->backendUser->user['uid']);
}
/**
* Checks if a user is logged in and the session is active.
*
* @return bool
*/
protected function isAuthorizedBackendSession()
{
return !empty($this->backendUser->user['uid']);
}
}
@@ -0,0 +1,64 @@
<?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\Core\FormProtection;
/**
* This class is a dummy implementation of the form protection,
* which is used when no authentication is used.
*/
class DisabledFormProtection extends AbstractFormProtection
{
/**
* Disable parent method
*
* @param string $formName
* @param string $action
* @param string $formInstanceName
* @return string
*/
public function generateToken($formName, $action = '', $formInstanceName = '')
{
return 'dummyToken';
}
/**
* Disable parent method.
* Always return TRUE.
*
* @param string $tokenId
* @param string $formName
* @param string $action
* @param string $formInstanceName
* @return bool
*/
public function validateToken($tokenId, $formName, $action = '', $formInstanceName = '')
{
return true;
}
/**
* Dummy implementation
*/
protected function retrieveSessionToken(): string
{
return '';
}
/**
* Dummy implementation
*/
public function persistSessionToken() {}
}
+21
View File
@@ -0,0 +1,21 @@
<?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\Core\FormProtection;
/**
* Invalid token exception
*/
class Exception extends \UnexpectedValueException {}
@@ -0,0 +1,215 @@
<?php
declare(strict_types=1);
/*
* 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\Core\FormProtection;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
/**
* This class creates and manages instances of the various form protection classes.
*
* Previously this class provides only provided static methods and could not be instantiated.
*
* Since TYPO3 v12, this class can and should be used as a factory to be injected into other
* controllers or middlewares, to handle FormProtections for HTTP Requests.
*/
readonly class FormProtectionFactory
{
public function __construct(
protected FlashMessageService $flashMessageService,
protected LanguageServiceFactory $languageServiceFactory,
protected FrontendInterface $runtimeCache,
protected ContainerInterface $container,
) {}
/**
* Method should be used whenever you do not have direct access to the request object.
* It is however recommended to use createFromRequest() whenever you have a PSR-7
* request object available.
*/
public function createForType(string $type): AbstractFormProtection
{
if (!in_array($type, ['installtool', 'frontend', 'backend', 'disabled'], true)) {
$type = 'disabled';
}
$identifier = $this->getIdentifierForType($type);
if ($this->runtimeCache->has($identifier)) {
return $this->runtimeCache->get($identifier);
}
$classNameAndConstructorArguments = $this->getClassNameAndConstructorArguments($type, $GLOBALS['TYPO3_REQUEST'] ?? null);
$this->runtimeCache->set($identifier, $this->createInstance(...$classNameAndConstructorArguments));
return $this->runtimeCache->get($identifier);
}
/**
* Detect the right FormProtection implementation based on the request.
*/
public function createFromRequest(ServerRequestInterface $request): AbstractFormProtection
{
$type = $this->determineTypeFromRequest($request);
$identifier = $this->getIdentifierForType($type);
if ($this->runtimeCache->has($identifier)) {
return $this->runtimeCache->get($identifier);
}
$classNameAndConstructorArguments = $this->getClassNameAndConstructorArguments($type, $request);
$this->runtimeCache->set($identifier, $this->createInstance(...$classNameAndConstructorArguments));
return $this->runtimeCache->get($identifier);
}
/**
* Detects the type of FormProtection which should be instantiated, based on the request.
*/
protected function determineTypeFromRequest(ServerRequestInterface $request): string
{
if ($this->isInstallToolSession($request)) {
return 'installtool';
}
if ($this->isFrontendSession($request)) {
return 'frontend';
}
if ($this->isBackendSession()) {
return 'backend';
}
return 'disabled';
}
/**
* This is the equivalent to getClassNameAndConstructorArgumentsByType() but non-static.
* It also does not handle "default" or class names, but is based on types previously resolved by
* the request. See determineTypeFromRequest()
*
* @param string $type Valid types: installtool, frontend, backend.
* @return array Array of arguments
*/
protected function getClassNameAndConstructorArguments(string $type, ?ServerRequestInterface $request): array
{
if ($type === 'installtool') {
return [
InstallToolFormProtection::class,
];
}
if ($type === 'frontend') {
$user = $request?->getAttribute('frontend.user');
if ($user && isset($user->user['uid'])) {
return [
FrontendFormProtection::class,
$user,
];
}
}
if ($type === 'backend') {
$user = $GLOBALS['BE_USER'] ?? null;
$isAjaxCall = (bool)($request ? $request->getAttribute('route')?->getOption('ajax') : false);
if ($user && isset($user->user['uid'])) {
return [
BackendFormProtection::class,
$user,
$this->container->get(Registry::class),
$this->getMessageClosure(
$this->languageServiceFactory->createFromUserPreferences($user),
$this->flashMessageService->getMessageQueueByIdentifier(),
$isAjaxCall
),
];
}
}
// failed to use preferred type, disable form protection
return [
DisabledFormProtection::class,
];
}
/**
* Conveniant method to create a deterministic cache identifier.
*/
protected function getIdentifierForType(string $type): string
{
return 'formprotection-instance-' . hash('xxh3', $type);
}
/**
* Check if we are in the install tool
*/
protected function isInstallToolSession(ServerRequestInterface $request): bool
{
return (bool)((int)$request->getAttribute('applicationType') & SystemEnvironmentBuilder::REQUESTTYPE_INSTALL);
}
/**
* Checks if a user is logged in and the session is active.
*/
protected function isBackendSession(): bool
{
$user = $GLOBALS['BE_USER'] ?? null;
return $user instanceof BackendUserAuthentication && isset($user->user['uid']);
}
/**
* Checks if a frontend user is logged in and the session is active.
*/
protected function isFrontendSession(ServerRequestInterface $request): bool
{
$user = $request->getAttribute('frontend.user');
return $user instanceof FrontendUserAuthentication && isset($user->user['uid']);
}
protected function getMessageClosure(LanguageService $languageService, FlashMessageQueue $messageQueue, bool $isAjaxCall): \Closure
{
return static function () use ($languageService, $messageQueue, $isAjaxCall) {
$flashMessage = new FlashMessage(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.formProtection.tokenInvalid'),
'',
ContextualFeedbackSeverity::ERROR,
!$isAjaxCall
);
$messageQueue->enqueue($flashMessage);
};
}
/**
* Creates an instance for the requested class $className
* and stores it internally.
*
* @param class-string $className
* @param array<int,mixed> $constructorArguments
* @throws \InvalidArgumentException
*/
protected function createInstance(string $className, ...$constructorArguments): AbstractFormProtection
{
if (!class_exists($className)) {
throw new \InvalidArgumentException('$className must be the name of an existing class, but actually was "' . $className . '".', 1285352962);
}
$instance = GeneralUtility::makeInstance($className, ...$constructorArguments);
if (!$instance instanceof AbstractFormProtection) {
throw new \InvalidArgumentException('$className must be a subclass of ' . AbstractFormProtection::class . ', but actually was "' . $className . '".', 1285353026);
}
return $instance;
}
}
@@ -0,0 +1,131 @@
<?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\Core\FormProtection;
use TYPO3\CMS\Core\Error\Exception;
use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
/**
* This class provides protection against cross-site request forgery (XSRF/CSRF)
* for actions in the frontend that change data.
*
* How to use:
*
* For each form (or link that changes some data), create a token and
* insert is as a hidden form element or use it as GET argument. The name of the form element does not
* matter; you only need it to get the form token for verifying it.
*
* <pre>
* $formToken = GeneralUtility::makeInstance(FormProtectionFactory::class)->createFromType('frontend')
* ->generateToken(
* 'User setup', 'edit'
* );
* $this->content .= '<input type="hidden" name="formToken" value="' .
* $formToken . '" />';
* </pre>
*
* The three parameters $formName, $action and $formInstanceName can be
* arbitrary strings, but they should make the form token as specific as
* possible. For different forms (e.g. User setup and editing a news
* record) or different records (with different UIDs) from the same table,
* those values should be different.
*
* For editing a news record, the call could look like this:
*
* <pre>
* $formToken = GeneralUtility::makeInstance(FormProtectionFactory::class)->createFromType('frontend')
* ->getFormProtection()->generateToken(
* 'news', 'edit', $uid
* );
* </pre>
*
*
* When processing the data that has been submitted by the form, you can check
* that the form token is valid like this:
*
* <pre>
* if ($dataHasBeenSubmitted && GeneralUtility::makeInstance(FormProtectionFactory::class)->createFromType('frontend')
* ->validateToken(
* \TYPO3\CMS\Core\Utility\GeneralUtility::_POST('formToken'),
* 'User setup', 'edit
* )
* ) {
* Processes the data.
* } else {
* Create a flash message for the invalid token or just discard this request.
* }
* </pre>
*/
class FrontendFormProtection extends AbstractFormProtection
{
/**
* Keeps the instance of the user which existed during creation
* of the object.
*
* @var FrontendUserAuthentication
*/
protected $frontendUser;
/**
* Only allow construction if we have an authorized frontend session
*
* @throws \TYPO3\CMS\Core\Error\Exception
*/
public function __construct(FrontendUserAuthentication $frontendUser, ?\Closure $validationFailedCallback = null)
{
$this->frontendUser = $frontendUser;
$this->validationFailedCallback = $validationFailedCallback;
if (!$this->isAuthorizedFrontendSession()) {
throw new Exception('A front-end form protection may only be instantiated if there is an active front-end session.', 1460975777);
}
}
/**
* Retrieves the saved session token or generates a new one.
*
* @return string
*/
protected function retrieveSessionToken()
{
$this->sessionToken = $this->frontendUser->getSessionData('formProtectionSessionToken');
if (empty($this->sessionToken)) {
$this->sessionToken = $this->generateSessionToken();
$this->persistSessionToken();
}
return $this->sessionToken;
}
/**
* Saves the tokens so that they can be used by a later incarnation of this
* class.
*
* @internal
*/
public function persistSessionToken()
{
$this->frontendUser->setAndSaveSessionData('formProtectionSessionToken', $this->sessionToken);
}
/**
* Checks if a user is logged in and the session is active.
*
* @return bool
*/
protected function isAuthorizedFrontendSession()
{
return !empty($this->frontendUser->user['uid']);
}
}
@@ -0,0 +1,84 @@
<?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\Core\FormProtection;
/**
* This class provides protection against cross-site request forgery (XSRF/CSRF)
* in the install tool.
*
*
* How to use this in the install tool:
*
* For each form in the install tool (or link that changes some data), create a
* token and insert is as a hidden form element. The name of the form element
* does not matter; you only need it to get the form token for verifying it.
*
* <pre>
* $formToken = $this->formProtection->generateToken(
* 'installToolPassword', 'change'
* );
* then puts the generated form token in a hidden field in the template
* </pre>
*
* The three parameters $formName, $action and $formInstanceName can be
* arbitrary strings, but they should make the form token as specific as
* possible. For different forms (e.g. the password change and editing a the
* configuration), those values should be different.
*
* When processing the data that has been submitted by the form, you can check
* that the form token is valid like this:
*
* <pre>
* if ($dataHasBeenSubmitted && $this->formProtection()->validateToken(
* $_POST['formToken'],
* 'installToolPassword',
* 'change'
* ) {
* processes the data
* } else {
* no need to do anything here as the install tool form protection will
* create an error message for an invalid token
* }
* </pre>
*/
/**
* Install Tool form protection
*/
class InstallToolFormProtection extends AbstractFormProtection
{
/**
* Retrieves or generates the session token.
*/
protected function retrieveSessionToken(): string
{
if (isset($_SESSION['installToolFormToken']) && !empty($_SESSION['installToolFormToken'])) {
$this->sessionToken = $_SESSION['installToolFormToken'];
} else {
$this->sessionToken = $this->generateSessionToken();
$this->persistSessionToken();
}
return $this->sessionToken;
}
/**
* Saves the tokens so that they can be used by a later incarnation of this
* class.
*/
public function persistSessionToken()
{
$_SESSION['installToolFormToken'] = $this->sessionToken;
}
}