Commit 19885966 authored by Frank Nägler's avatar Frank Nägler Committed by Oliver Hader
Browse files

[SECURITY] Add cache for error page handling

To prevent DoS attacks by using page-based error handling, the
content of the error page is now cached, this prevents fetching
the content of the error pages again and again.

Resolves: #88824
Releases: master, 11.1, 10.4, 9.5
Change-Id: I6dea5200dc710a182b66deedfbeb2110ea829117
Security-Bulletin: TYPO3-CORE-SA-2021-005
Security-References: CVE-2021-21359
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/68415

Tested-by: Oliver Hader's avatarOliver Hader <oliver.hader@typo3.org>
Reviewed-by: Oliver Hader's avatarOliver Hader <oliver.hader@typo3.org>
parent 46266e60
......@@ -18,8 +18,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\LinkHandling\LinkService;
use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
use TYPO3\CMS\Core\Site\Entity\Site;
......@@ -44,6 +47,11 @@ class PageContentErrorHandler implements PageErrorHandlerInterface
*/
protected $errorHandlerConfiguration;
/**
* @var int
*/
protected $pageUid = 0;
/**
* PageContentErrorHandler constructor.
* @param int $statusCode
......@@ -65,26 +73,71 @@ 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;
$report = [];
if ($resolvedUrl !== (string)$request->getUri()) {
$content = GeneralUtility::getUrl($resolvedUrl, 0, null, $report);
if ($content === false && ((int)$report['error'] === -1 || (int)$report['error'] > 200)) {
throw new \RuntimeException('Error handler could not fetch error page "' . $resolvedUrl . '", reason: ' . $report['message'], 1544172838);
$cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_pages');
$cacheIdentifier = 'errorPage_' . md5($resolvedUrl);
$cacheContent = $cache->get($cacheIdentifier);
if (!$cacheContent && $resolvedUrl !== (string)$request->getUri()) {
try {
$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);
}
$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']);
}
} catch (InvalidRouteArgumentsException | SiteNotFoundException $e) {
$content = 'Invalid error handler configuration: ' . $this->errorHandlerConfiguration['errorContentSource'];
}
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)
*
......@@ -105,8 +158,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);
......
.. 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
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment