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\
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_
in case request served with plain HTTP[hash] __
in case request served with secured HTTPSSecure- typo3nonce_ [hash]
Submitting request token value to application:
- HTTP body, for example in
<form>
via parameter__
Request Token - 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\
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
Request
andToken Nonce
objects (later created implicitly in this example) are organized in the\TYPO3\
.CMS\ Core\ Context\ Security Aspect <?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 } }
<!-- Assign request token object for ViewHelper --> <f:form action="process" requestToken="{requestToken}"> ... </f:form>
Copied!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>
Copied! -
Invoke action request and provide nonce and request token values
When submitting the form and invoking the corresponding action, same-site cookies
typo3nonce_
and request-token value[hash] __
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.Request Token The middleware
\TYPO3\
takes care of providing the received nonce and received request token values inCMS\ Core\ Middleware\ Request Token Middleware \TYPO3\
. The handling controller action needs to verify that the request token has the expectedCMS\ Core\ Context\ Security Aspect 'my/
scope.process' <?php use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Context\SecurityAspect; use TYPO3\CMS\Core\Utility\GeneralUtility; final class MyController { public function showFormAction() { // for the implementation, see above } public function processAction() { $context = GeneralUtility::makeInstance(Context::class); $securityAspect = SecurityAspect::provideIn($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 cookie in case no other // nonce value shall be emitted during the current HTTP request if ($requestToken->getSigningSecretIdentifier() !== null) { $securityAspect->getSigningSecretResolver()->revokeIdentifier( $requestToken->getSigningSecretIdentifier(), ); } } } }