‪TYPO3CMS  ‪main
RequestTokenMiddleware.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Psr\Http\Message\ResponseInterface;
21 use Psr\Http\Message\ServerRequestInterface;
22 use Psr\Http\Server\MiddlewareInterface;
23 use Psr\Http\Server\RequestHandlerInterface;
24 use Psr\Log\LoggerAwareInterface;
25 use Psr\Log\LoggerAwareTrait;
26 use Symfony\Component\HttpFoundation\Cookie;
36 
40 class ‪RequestTokenMiddleware implements MiddlewareInterface, LoggerAwareInterface
41 {
42  use LoggerAwareTrait;
43 
44  protected const ‪COOKIE_PREFIX = 'typo3nonce_';
45  protected const ‪SECURE_PREFIX = '__Secure-';
46 
47  protected const ‪ALLOWED_METHODS = ['POST', 'PUT', 'PATCH'];
48 
51 
52  public function ‪__construct(‪Context $context)
53  {
54  $this->securityAspect = ‪SecurityAspect::provideIn($context);
55  $this->noncePool = $this->securityAspect->getNoncePool();
56  }
57 
58  public function ‪process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
59  {
60  // @todo someā„¢ route handling mechanism might verify request-tokens (-> e.g. backend-routes, unsure for frontend)
61  $this->noncePool->merge($this->‪resolveNoncePool($request))->purge();
62 
63  try {
64  $this->securityAspect->setReceivedRequestToken($this->‪resolveReceivedRequestToken($request));
65  } catch (‪RequestTokenException $exception) {
66  // request token was given, but could not be verified
67  $this->securityAspect->setReceivedRequestToken(false);
68  $this->logger->debug('Could not resolve request token', ['exception' => $exception]);
69  }
70 
71  $response = $handler->handle($request);
72  return $this->‪enrichResponseWithCookie($request, $response);
73  }
74 
75  protected function ‪resolveNoncePool(ServerRequestInterface $request): ‪NoncePool
76  {
77  $secure = $this->‪isHttps($request);
78  // resolves cookie name dependent on whether TLS is used in request and uses `__Secure-` prefix,
79  // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#cookie_prefixes
80  $securePrefix = $secure ? ‪self::SECURE_PREFIX : '';
81  $cookiePrefix = $securePrefix . ‪self::COOKIE_PREFIX;
82  $cookiePrefixLength = strlen($cookiePrefix);
83  $cookies = array_filter(
84  $request->getCookieParams(),
85  static fn(mixed $name): bool => is_string($name) && str_starts_with($name, $cookiePrefix),
86  ARRAY_FILTER_USE_KEY
87  );
88  $items = [];
89  foreach ($cookies as $name => $value) {
90  $name = substr($name, $cookiePrefixLength);
91  try {
92  $items[$name] = ‪Nonce::fromHashSignedJwt($value);
93  } catch (‪NonceException $exception) {
94  $this->logger->debug('Could not resolve received nonce', ['exception' => $exception]);
95  $items[$name] = null;
96  }
97  }
98  // @todo pool `$options` should be configurable via `$TYPO3_CONF_VARS`
99  return GeneralUtility::makeInstance(NoncePool::class, $items);
100  }
101 
105  protected function ‪resolveReceivedRequestToken(ServerRequestInterface $request): ?‪RequestToken
106  {
107  $headerValue = $request->getHeaderLine(‪RequestToken::HEADER_NAME);
108  $paramValue = (string)($request->getParsedBody()[‪RequestToken::PARAM_NAME] ?? '');
109  if ($headerValue !== '') {
110  $tokenValue = $headerValue;
111  } elseif (in_array($request->getMethod(), self::ALLOWED_METHODS, true)) {
112  $tokenValue = $paramValue;
113  } else {
114  $tokenValue = '';
115  }
116  if ($tokenValue === '') {
117  return null;
118  }
119  return ‪RequestToken::fromHashSignedJwt($tokenValue, $this->securityAspect->getSigningSecretResolver());
120  }
121 
122  protected function ‪enrichResponseWithCookie(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
123  {
124  $secure = $this->‪isHttps($request);
125  $normalizedParams = $request->getAttribute('normalizedParams');
126  $path = $normalizedParams->getSitePath();
127  $securePrefix = $secure ? ‪self::SECURE_PREFIX : '';
128  $cookiePrefix = $securePrefix . ‪self::COOKIE_PREFIX;
129 
130  $createCookie = static fn(string $name, string $value, int $expire): Cookie => new Cookie(
131  $name,
132  $value,
133  $expire,
134  $path,
135  null,
136  $secure,
137  true,
138  false,
139  Cookie::SAMESITE_STRICT
140  );
141 
142  $cookies = [];
143  // emit new nonce cookies
144  foreach ($this->noncePool->getEmittableNonces() as $name => $nonce) {
145  $cookies[] = $createCookie($cookiePrefix . $name, $nonce->toHashSignedJwt(), 0);
146  }
147  // revoke nonce cookies (exceeded pool size, expired or explicitly revoked)
148  foreach ($this->noncePool->getRevocableNames() as $name) {
149  $cookies[] = $createCookie($cookiePrefix . $name, '', -1);
150  }
151  // finally apply to response
152  foreach ($cookies as $cookie) {
153  $response = $response->withAddedHeader('Set-Cookie', (string)$cookie);
154  }
155  return $response;
156  }
157 
158  protected function ‪isHttps(ServerRequestInterface $request): bool
159  {
160  $normalizedParams = $request->getAttribute('normalizedParams');
161  return $normalizedParams instanceof ‪NormalizedParams && $normalizedParams->‪isHttps();
162  }
163 }
‪TYPO3\CMS\Core\Context\SecurityAspect\provideIn
‪static provideIn(Context $context)
Definition: SecurityAspect.php:41
‪TYPO3\CMS\Core\Security\RequestToken\HEADER_NAME
‪const HEADER_NAME
Definition: RequestToken.php:29
‪TYPO3\CMS\Core\Middleware\RequestTokenMiddleware\resolveReceivedRequestToken
‪resolveReceivedRequestToken(ServerRequestInterface $request)
Definition: RequestTokenMiddleware.php:105
‪TYPO3\CMS\Core\Middleware\RequestTokenMiddleware\ALLOWED_METHODS
‪const ALLOWED_METHODS
Definition: RequestTokenMiddleware.php:47
‪TYPO3\CMS\Core\Security\RequestTokenException
Definition: RequestTokenException.php:25
‪TYPO3\CMS\Core\Middleware\RequestTokenMiddleware\COOKIE_PREFIX
‪const COOKIE_PREFIX
Definition: RequestTokenMiddleware.php:44
‪TYPO3\CMS\Core\Middleware\RequestTokenMiddleware\__construct
‪__construct(Context $context)
Definition: RequestTokenMiddleware.php:52
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:54
‪TYPO3\CMS\Core\Middleware\RequestTokenMiddleware\process
‪process(ServerRequestInterface $request, RequestHandlerInterface $handler)
Definition: RequestTokenMiddleware.php:58
‪TYPO3\CMS\Core\Security\RequestToken
Definition: RequestToken.php:26
‪TYPO3\CMS\Core\Context\SecurityAspect
Definition: SecurityAspect.php:30
‪TYPO3\CMS\Core\Middleware\RequestTokenMiddleware\SECURE_PREFIX
‪const SECURE_PREFIX
Definition: RequestTokenMiddleware.php:45
‪TYPO3\CMS\Core\Middleware\RequestTokenMiddleware
Definition: RequestTokenMiddleware.php:41
‪TYPO3\CMS\Core\Security\NoncePool
Definition: NoncePool.php:24
‪TYPO3\CMS\Core\Middleware\RequestTokenMiddleware\resolveNoncePool
‪resolveNoncePool(ServerRequestInterface $request)
Definition: RequestTokenMiddleware.php:75
‪TYPO3\CMS\Core\Http\NormalizedParams\isHttps
‪bool isHttps()
Definition: NormalizedParams.php:340
‪TYPO3\CMS\Core\Security\Nonce\fromHashSignedJwt
‪static fromHashSignedJwt(string $jwt)
Definition: Nonce.php:42
‪TYPO3\CMS\Core\Middleware\RequestTokenMiddleware\enrichResponseWithCookie
‪enrichResponseWithCookie(ServerRequestInterface $request, ResponseInterface $response)
Definition: RequestTokenMiddleware.php:122
‪TYPO3\CMS\Core\Security\RequestToken\PARAM_NAME
‪const PARAM_NAME
Definition: RequestToken.php:28
‪TYPO3\CMS\Core\Middleware
Definition: AbstractContentSecurityPolicyReporter.php:18
‪TYPO3\CMS\Core\Security\NonceException
Definition: NonceException.php:25
‪TYPO3\CMS\Core\Security\Nonce
Definition: Nonce.php:29
‪TYPO3\CMS\Core\Middleware\RequestTokenMiddleware\$noncePool
‪NoncePool $noncePool
Definition: RequestTokenMiddleware.php:50
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Middleware\RequestTokenMiddleware\$securityAspect
‪SecurityAspect $securityAspect
Definition: RequestTokenMiddleware.php:49
‪TYPO3\CMS\Core\Middleware\RequestTokenMiddleware\isHttps
‪isHttps(ServerRequestInterface $request)
Definition: RequestTokenMiddleware.php:158
‪TYPO3\CMS\Core\Http\NormalizedParams
Definition: NormalizedParams.php:38
‪TYPO3\CMS\Core\Security\RequestToken\fromHashSignedJwt
‪static fromHashSignedJwt(string $jwt, SigningSecretInterface|SigningSecretResolver $secret)
Definition: RequestToken.php:48