diff --git a/typo3/sysext/core/Classes/Error/PageErrorHandler/PageContentErrorHandler.php b/typo3/sysext/core/Classes/Error/PageErrorHandler/PageContentErrorHandler.php index 1562280f02f6782fd33283bcd7e0d00570cd6315..f40590c9079d9bbf296f4099de4fa3b75c535eac 100644 --- a/typo3/sysext/core/Classes/Error/PageErrorHandler/PageContentErrorHandler.php +++ b/typo3/sysext/core/Classes/Error/PageErrorHandler/PageContentErrorHandler.php @@ -19,10 +19,11 @@ namespace TYPO3\CMS\Core\Error\PageErrorHandler; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Cache\CacheManager; +use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException; use TYPO3\CMS\Core\Exception\SiteNotFoundException; use TYPO3\CMS\Core\Http\HtmlResponse; use TYPO3\CMS\Core\Http\RequestFactory; -use TYPO3\CMS\Core\Http\Response; use TYPO3\CMS\Core\LinkHandling\LinkService; use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException; use TYPO3\CMS\Core\Site\Entity\Site; @@ -47,6 +48,11 @@ class PageContentErrorHandler implements PageErrorHandlerInterface */ protected $errorHandlerConfiguration; + /** + * @var int + */ + protected $pageUid = 0; + /** * PageContentErrorHandler constructor. * @param int $statusCode @@ -68,25 +74,48 @@ class PageContentErrorHandler implements PageErrorHandlerInterface * @param array $reasons * @return ResponseInterface * @throws \RuntimeException + * @throws NoSuchCacheException */ public function handlePageError(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface { try { $resolvedUrl = $this->resolveUrl($request, $this->errorHandlerConfiguration['errorContentSource']); - $content = null; - if ($resolvedUrl !== (string)$request->getUri()) { + + $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('pages'); + $cacheIdentifier = 'errorPage_' . md5($resolvedUrl); + $cacheContent = $cache->get($cacheIdentifier); + + if (!$cacheContent && $resolvedUrl !== (string)$request->getUri()) { try { - $subResponse = GeneralUtility::makeInstance(RequestFactory::class)->request($resolvedUrl, 'GET'); + $subResponse = GeneralUtility::makeInstance(RequestFactory::class) + ->request($resolvedUrl, 'GET', $this->getSubRequestOptions()); } catch (\Exception $e) { throw new \RuntimeException('Error handler could not fetch error page "' . $resolvedUrl . '", reason: ' . $e->getMessage(), 1544172838); } if ($subResponse->getStatusCode() >= 300) { throw new \RuntimeException('Error handler could not fetch error page "' . $resolvedUrl . '", status code: ' . $subResponse->getStatusCode(), 1544172839); } - // create new response object and re-use only the body and the content-type of the sub-request - return new Response($subResponse->getBody(), $this->statusCode, [ - 'Content-Type' => $subResponse->getHeader('Content-Type') - ]); + + $body = $subResponse->getBody()->getContents(); + $contentType = $subResponse->getHeader('Content-Type'); + + // Cache body and content-type if sub-response returned a HTTP status 200 + if ($subResponse->getStatusCode() === 200) { + $cacheTags = ['errorPage']; + if ($this->pageUid > 0) { + // Cache Tag "pageId_" ensures, cache is purged when content of 404 page changes + $cacheTags[] = 'pageId_' . $this->pageUid; + } + $cacheContent = [ + 'body' => $body, + 'headers' => ['Content-Type' => $contentType], + ]; + $cache->set($cacheIdentifier, $cacheContent, $cacheTags); + } + } + if ($cacheContent && $cacheContent['body'] && $cacheContent['headers']) { + // We use a HtmlResponse here, since no Stream is available for cached response content + return new HtmlResponse($cacheContent['body'], $this->statusCode, $cacheContent['headers']); } $content = 'The error page could not be resolved, as the error page itself is not accessible'; } catch (InvalidRouteArgumentsException | SiteNotFoundException $e) { @@ -95,6 +124,22 @@ class PageContentErrorHandler implements PageErrorHandlerInterface return new HtmlResponse($content, $this->statusCode); } + /** + * Returns request options for the subrequest and ensures, that a reasoneable timeout is present + * + * @return array|int[] + */ + protected function getSubRequestOptions(): array + { + $options = []; + if ((int)$GLOBALS['TYPO3_CONF_VARS']['HTTP']['timeout'] === 0) { + $options = [ + 'timeout' => 30 + ]; + } + return $options; + } + /** * Resolve the URL (currently only page and external URL are supported) * @@ -115,8 +160,10 @@ class PageContentErrorHandler implements PageErrorHandlerInterface return $urlParams['url']; } + $this->pageUid = (int)$urlParams['pageuid']; + // Get the site related to the configured error page - $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId((int)$urlParams['pageuid']); + $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($this->pageUid); // Fall back to current request for the site if (!$site instanceof Site) { $site = $request->getAttribute('site', null); diff --git a/typo3/sysext/core/Documentation/Changelog/master/Important-88824-AddCacheForErrorPageHandling.rst b/typo3/sysext/core/Documentation/Changelog/master/Important-88824-AddCacheForErrorPageHandling.rst new file mode 100644 index 0000000000000000000000000000000000000000..2688a87e25d178b6a0ef97381de79bd5aa90bbbe --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Important-88824-AddCacheForErrorPageHandling.rst @@ -0,0 +1,24 @@ +.. include:: ../../Includes.txt + +===================================================== +Important: #88824 - Add cache for error page handling +===================================================== + +See :issue:`88824` + +Description +=========== + +In order to prevent possible DoS attacks when the page-based error handler +is used, the content of the 404 error page is now cached in the TYPO3 +page cache. Any dynamic content on the error page (e.g. content created +by TypoScript or uncached plugins) will therefore also be cached. + +If the 404 error page contains dynamic content, TYPO3 administrators must +ensure that no sensitive data (e.g. username of logged in frontend user) +will be shown on the error page. + +If dynamic content is required on the 404 error page, it is recommended +to implement a custom PHP based error handler. + +.. index:: Backend, ext:backend