CSRF-like request token handling¶
New in version 12.0.
A CSRF-like request token handling is available to mitigate potential cross-site requests on actions with side effects. This approach does not require an existing server-side user session, but uses a nonce as a "pre-session". The main scope is to ensure a user actually has visited a page, before submitting data to the webserver.
This token can only be used for HTTP methods POST
, PUT
or PATCH
, but
for instance not for GET
request.
The \TYPO3\CMS\Core\Middleware\RequestTokenMiddleware
resolves
request tokens and nonce values from a request and enhances responses with
a nonce value in case the underlying application issues one. Both items are
serialized as a JSON Web Token (JWT) hash signed with HS256
. Request tokens
use the provided nonce value during signing.
Session cookie names involved for providing the nonce value:
typo3nonce_[hash]
in case request served with plain HTTP__Secure-typo3nonce_[hash]
in case request served with secured HTTPS
Submitting request token value to application:
HTTP body, for example in
<form>
via parameter__RequestToken
HTTP header, for example in XHR via header
X-TYPO3-Request-Token
Attention
When working with multiple browser tabs, an existing nonce value (stored as session cookie in the browser of the user) might be overridden.
Note
The current concept uses the \TYPO3\CMS\Core\Security\NoncePool
which
supports five different nonces in the same request. The pool purges nonces
15 minutes (900 seconds) after they have been issued.
See also
The event BeforeRequestTokenProcessedEvent is available to intercept/adjust the request token.
Workflow¶
The sequence looks like the following:
Retrieve nonce and request token values
This happens on the previous legitimate visit on a page that offers a corresponding form that shall be protected. The
RequestToken
andNonce
objects (later created implicitly in this example) are organized in the\TYPO3\CMS\Core\Context\SecurityAspect
.EXT:my_extension/Classes/Controller/MyController.php¶use TYPO3\CMS\Core\Security\RequestToken; use TYPO3\CMS\Fluid\View\StandaloneView; final class MyController { private StandaloneView $view; public function showFormAction() { // creating new request token with scope 'my/process' and hand over to view $requestToken = RequestToken::create('my/process'); $this->view->assign('requestToken', $requestToken); // ... } public function processAction() { // for the implementation, see below } }
EXT:my_extension/Resources/Private/Templates/ShowForm.html¶<!-- Assign request token object for ViewHelper --> <f:form action="process" requestToken="{requestToken}> ... </f:form>
The HTTP response on calling the shown controller action above will be like this:
HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Set-Cookie: typo3nonce_[hash]=[nonce-as-jwt]; path=/; httponly; samesite=strict ... <form action="/my/process" method="post"> ... <input type="hidden" name="__request_token" value="[request-token-as-jwt]"> ... </form>
Invoke action request and provide nonce and request token values
When submitting the form and invoking the corresponding action, same-site cookies
typo3nonce_[hash]
and request-token value__RequestToken
are sent back to the server. Without using a separate nonce in a scope that is protected by the client, the corresponding request token could be easily extracted from markup and used without having the possibility to verify the procedural integrity.The middleware
\TYPO3\CMS\Core\Middleware\RequestTokenMiddleware
takes care of providing the received nonce and received request token values in\TYPO3\CMS\Core\Context\SecurityAspect
. The handling controller action needs to verify that the request token has the expected'my/process'
scope.EXT:my_extension/Classes/Controller/MyController.php¶use TYPO3\CMS\Core\Context\SecurityAspect; final class MyController { private Context $context; public function showFormAction() { // for the implementation, see above } public function processAction() { $securityAspect = SecurityAspect::provideIn($this->context); $requestToken = $securityAspect->getReceivedRequestToken(); if ($requestToken === null) { // No request token was provided in the request // for example, (overridden) templates need to be adjusted } elseif ($requestToken === false) { // There was a request token, which could not be verified with the nonce // for example, when nonce cookie has been overridden by another HTTP request } elseif ($requestToken->scope !== 'my/process') { // There was a request token, but for a different scope // for example, when a form with different scope was submitted } else { // The request token was valid and for the expected scope $this->doTheMagic(); // The middleware takes care to remove the the cookie in case no other // nonce value shall be emitted during the current HTTP request $requestToken->getSigningSecretIdentifier() !== null) { $securityAspect->getSigningSecretResolver()->revokeIdentifier( $requestToken->getSigningSecretIdentifier() ); } } } }