<?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\Frontend\Middleware; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use Psr\Log\LogLevel; use TYPO3\CMS\Core\Http\RedirectResponse; use TYPO3\CMS\Core\Routing\PageArguments; use TYPO3\CMS\Core\TimeTracker\TimeTracker; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\HttpUtility; use TYPO3\CMS\Frontend\Controller\ErrorController; use TYPO3\CMS\Frontend\Page\CacheHashCalculator; use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons; /** * This middleware validates given request parameters against the common "cHash" functionality. */ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface { use LoggerAwareTrait; /** * The cHash Service class used for cHash related functionality * * @var CacheHashCalculator */ protected $cacheHashCalculator; /** * @var TimeTracker */ protected $timeTracker; /** * @var bool will be used to set $TSFE->no_cache later-on */ protected $disableCache = false; public function __construct( CacheHashCalculator $cacheHashCalculator, TimeTracker $timeTracker ) { $this->cacheHashCalculator = $cacheHashCalculator; $this->timeTracker = $timeTracker; } /** * Validates the &cHash parameter against the other $queryParameters / GET parameters * * @param ServerRequestInterface $request * @param RequestHandlerInterface $handler * @return ResponseInterface */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $this->disableCache = (bool)$request->getAttribute('noCache', false); $pageNotFoundOnValidationError = (bool)($GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError'] ?? true); /** @var PageArguments $pageArguments */ $pageArguments = $request->getAttribute('routing', null); if (!($pageArguments instanceof PageArguments)) { // Page Arguments must be set in order to validate. This middleware only works if PageArguments // is available, and is usually combined with the Page Resolver middleware return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( $request, 'Page Arguments could not be resolved', ['code' => PageAccessFailureReasons::INVALID_PAGE_ARGUMENTS] ); } if ($GLOBALS['TYPO3_CONF_VARS']['FE']['disableNoCacheParameter'] ?? true) { $cachingDisabledByRequest = false; } else { $cachingDisabledByRequest = $pageArguments->getArguments()['no_cache'] ?? $request->getParsedBody()['no_cache'] ?? false; } if (($cachingDisabledByRequest || $this->disableCache) && !$pageNotFoundOnValidationError) { // No need to test anything if caching was already disabled. return $handler->handle($request); } // Evaluate the cache hash parameter or dynamic arguments when coming from a Site-based routing $cHash = (string)($pageArguments->getArguments()['cHash'] ?? ''); $queryParams = $pageArguments->getDynamicArguments(); if ($cHash !== '' || !empty($queryParams)) { $relevantParametersForCacheHashArgument = $this->getRelevantParametersForCacheHashCalculation($pageArguments); if ($cHash !== '') { if (empty($relevantParametersForCacheHashArgument)) { // cHash was given, but nothing to be calculated, so let's do a redirect to the current page // but without the cHash $this->logger->notice('The incoming cHash "{hash}" is given but not needed. cHash is unset', ['hash' => $cHash]); $uri = $request->getUri(); unset($queryParams['cHash']); $uri = $uri->withQuery(HttpUtility::buildQueryString($queryParams)); return new RedirectResponse($uri, 308); } if (!$this->evaluateCacheHashParameter($cHash, $relevantParametersForCacheHashArgument, $pageNotFoundOnValidationError)) { return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( $request, 'Request parameters could not be validated (&cHash comparison failed)', ['code' => PageAccessFailureReasons::CACHEHASH_COMPARISON_FAILED] ); } // No cHash given but was required } elseif (!$this->evaluatePageArgumentsWithoutCacheHash($pageArguments, $pageNotFoundOnValidationError)) { return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( $request, 'Request parameters could not be validated (&cHash empty)', ['code' => PageAccessFailureReasons::CACHEHASH_EMPTY] ); } } $request = $request->withAttribute('noCache', $this->disableCache); return $handler->handle($request); } /** * Filters out the arguments that are necessary for calculating cHash * * @param PageArguments $pageArguments * @return array<string, string> */ protected function getRelevantParametersForCacheHashCalculation(PageArguments $pageArguments): array { $queryParams = $pageArguments->getDynamicArguments(); $queryParams['id'] = $pageArguments->getPageId(); return $this->cacheHashCalculator->getRelevantParameters(HttpUtility::buildQueryString($queryParams)); } /** * Calculates a hash string based on additional parameters in the url. * This is used to cache pages with more parameters than just id and type. * * @param string $cHash the chash to check * @param array<string, string> $relevantParameters GET parameters necessary for cHash calculation * @param bool $pageNotFoundOnCacheHashError see $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError'] * @return bool if false, then a PageNotFound response is triggered */ protected function evaluateCacheHashParameter(string $cHash, array $relevantParameters, bool $pageNotFoundOnCacheHashError): bool { $calculatedCacheHash = $this->cacheHashCalculator->calculateCacheHash($relevantParameters); if (hash_equals($calculatedCacheHash, $cHash)) { return true; } // Early return to trigger the error controller if ($pageNotFoundOnCacheHashError) { return false; } // Caching is disabled now (but no 404) $this->disableCache = true; $this->timeTracker->setTSlogMessage('The incoming cHash "' . $cHash . '" and calculated cHash "' . $calculatedCacheHash . '" did not match, so caching was disabled. The fieldlist used was "' . implode(',', array_keys($relevantParameters)) . '"', LogLevel::ERROR); return true; } /** * No cHash is set but there are query parameters, check if that is correct * * Should only be called if NO cHash parameter is given. * * @param array<string, string|array> $dynamicArguments * @param bool $pageNotFoundOnCacheHashError * @return bool */ protected function evaluateQueryParametersWithoutCacheHash(array $dynamicArguments, bool $pageNotFoundOnCacheHashError): bool { if (!$this->cacheHashCalculator->doParametersRequireCacheHash(HttpUtility::buildQueryString($dynamicArguments))) { return true; } // cHash is required, but not given, so trigger a 404 if ($pageNotFoundOnCacheHashError) { return false; } // Caching is disabled now (but no 404) $this->disableCache = true; $this->timeTracker->setTSlogMessage('TSFE->reqCHash(): No &cHash parameter was sent for GET vars though required so caching is disabled', LogLevel::ERROR); return true; } /** * No cHash is set but there are query parameters, then calculate a possible cHash from the given * query parameters and see if a cHash is returned (similar to comparing this). * * Is only called if NO cHash parameter is given. */ protected function evaluatePageArgumentsWithoutCacheHash(PageArguments $pageArguments, bool $pageNotFoundOnCacheHashError): bool { // legacy behaviour if (!($GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['enforceValidation'] ?? false)) { return $this->evaluateQueryParametersWithoutCacheHash($pageArguments->getDynamicArguments(), $pageNotFoundOnCacheHashError); } $relevantParameters = $this->getRelevantParametersForCacheHashCalculation($pageArguments); // There are parameters that would be needed for the current page, but no cHash is given. // Thus, a "page not found" error is thrown - as configured via "pageNotFoundOnCHashError". if (!empty($relevantParameters) && $pageNotFoundOnCacheHashError) { return false; } // There are no parameters that require a cHash. // We end up here when the site was called with an `id` param, e.g. https://example.org/index?id=123. // Avoid disabling caches in this case. if (empty($relevantParameters)) { return true; } // Caching is disabled now (but no 404) $this->disableCache = true; $this->timeTracker->setTSlogMessage('No &cHash parameter was sent for given query parameters, so caching is disabled', LogLevel::ERROR); return true; } }