Skip to content
Snippets Groups Projects
Commit 1e5f4441 authored by Benni Mack's avatar Benni Mack Committed by Oliver Hader
Browse files

[SECURITY] Avoid DoS when generating Error pages

TYPO3 now uses a lock strategy to avoid having to many request
waiting for the generation of the error page (which cannot be
generated via the external HTTP request, as there might be not
enough workers / PHP processes available during a DoS attack).
If a lock is in place, it directly returns a generic error
response instead of waiting for the lock or that the error
page is retrieved/rendered.

Additionally, if the external error page could not be retrieved
(HTTP status code other than 200), it will also create a generic
response and cache that instead. This avoids keeping requesting
for the errounous external HTTP page.

This could happen when using external HTTP requests (Guzzle) to
resolve an error page (via PageContentErrorHandler) for 404 sites.

Only TYPO3 installations using the feature "subrequestPageErrors"
via $TYPO3_CONF_VARS[SYS][features][subrequestPageErrors] = true
are not affected as the error page is generated during the
same PHP process, avoiding to create another external process.

Resolves: #98384
Releases: 11.5, 10.4
Change-Id: Iae1cae882707a519b2cef85112525ea213a72eef
Security-Bulletin: TYPO3-CORE-SA-2022-012
Security-References: CVE-2022-23500
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/77089


Tested-by: default avatarOliver Hader <oliver.hader@typo3.org>
Reviewed-by: default avatarOliver Hader <oliver.hader@typo3.org>
parent 6c6f137e
No related merge requests found
......@@ -17,12 +17,14 @@ declare(strict_types=1);
namespace TYPO3\CMS\Core\Error\PageErrorHandler;
use GuzzleHttp\Exception\ClientException;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Configuration\Features;
use TYPO3\CMS\Core\Controller\ErrorPageController;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\RequestFactory;
......@@ -30,6 +32,10 @@ use TYPO3\CMS\Core\Http\Response;
use TYPO3\CMS\Core\Http\Stream;
use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Core\LinkHandling\LinkService;
use TYPO3\CMS\Core\Locking\Exception\LockAcquireWouldBlockException;
use TYPO3\CMS\Core\Locking\LockFactory;
use TYPO3\CMS\Core\Locking\LockingStrategyInterface;
use TYPO3\CMS\Core\Messaging\AbstractMessage;
use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
......@@ -92,7 +98,7 @@ class PageContentErrorHandler implements PageErrorHandlerInterface
{
try {
$urlParams = $this->link->resolve($this->errorHandlerConfiguration['errorContentSource']);
$urlParams['pageuid'] = (int)($urlParams['pageuid'] ?? 0);
$this->pageUid = $urlParams['pageuid'] = (int)($urlParams['pageuid'] ?? 0);
$resolvedUrl = $this->resolveUrl($request, $urlParams);
// avoid denial-of-service amplification scenario
......@@ -105,13 +111,24 @@ class PageContentErrorHandler implements PageErrorHandlerInterface
if ($this->useSubrequest) {
// Create a subrequest and do not take any special query parameters into account
$subRequest = $request->withQueryParams([])->withUri(new Uri($resolvedUrl))->withMethod('GET');
$subResponse = $this->stashEnvironment(fn (): ResponseInterface => $this->sendSubRequest($subRequest, $urlParams['pageuid']));
$subResponse = $this->stashEnvironment(fn (): ResponseInterface => $this->sendSubRequest($subRequest, $this->pageUid));
} else {
$cacheIdentifier = 'errorPage_' . md5($resolvedUrl);
try {
$subResponse = $this->cachePageRequest($resolvedUrl, $this->pageUid, fn () => $this->sendRawRequest($resolvedUrl));
$subResponse = $this->cachePageRequest(
$this->pageUid,
fn () => $this->sendRawRequest($resolvedUrl),
$cacheIdentifier
);
} catch (\Exception $e) {
throw new \RuntimeException(sprintf('Error handler could not fetch error page "%s", reason: %s', $resolvedUrl, $e->getMessage()), 1544172838, $e);
}
// Ensure that 503 status code is kept, and not changed to 500.
if ($subResponse->getStatusCode() === 503) {
return $this->responseFactory->createResponse($subResponse->getStatusCode())
->withHeader('content-type', $subResponse->getHeader('content-type'))
->withBody($subResponse->getBody());
}
}
if ($subResponse->getStatusCode() >= 300) {
......@@ -144,40 +161,92 @@ class PageContentErrorHandler implements PageErrorHandlerInterface
/**
* Caches a subrequest fetch.
*/
protected function cachePageRequest(string $resolvedUrl, int $pageId, callable $fetcher): ResponseInterface
protected function cachePageRequest(int $pageId, callable $fetcher, string $cacheIdentifier): ResponseInterface
{
$cacheIdentifier = 'errorPage_' . md5($resolvedUrl);
$responseData = $this->cache->get($cacheIdentifier);
if (!is_array($responseData)) {
if (is_array($responseData) && $responseData !== []) {
return $this->createCachedPageRequestResponse($responseData);
}
$cacheTags = [];
$cacheTags[] = 'errorPage';
if ($pageId > 0) {
// Cache Tag "pageId_" ensures, cache is purged when content of 404 page changes
$cacheTags[] = 'pageId_' . $pageId;
}
$lockFactory = GeneralUtility::makeInstance(LockFactory::class);
$lock = $lockFactory->createLocker(
$cacheIdentifier,
LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK
);
try {
$locked = $lock->acquire(
LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK
);
if (!$locked) {
return $this->createGenericErrorResponse('Lock could not be acquired.');
}
/** @var ResponseInterface $response */
$response = $fetcher();
$cacheTags = [];
if ($response->getStatusCode() === 200) {
$cacheTags[] = 'errorPage';
if ($pageId > 0) {
// Cache Tag "pageId_" ensures, cache is purged when content of 404 page changes
$cacheTags[] = 'pageId_' . $pageId;
}
$responseData = [
'headers' => $response->getHeaders(),
'body' => $response->getBody()->getContents(),
'reasonPhrase' => $response->getReasonPhrase(),
];
$this->cache->set($cacheIdentifier, $responseData, $cacheTags);
if ($response->getStatusCode() !== 200) {
// External request lead to an error. Create a generic error response,
// cache and use that instead of the external error response.
$response = $this->createGenericErrorResponse('External error page could not be retrieved.');
}
} else {
$body = new Stream('php://temp', 'wb+');
$body->write($responseData['body'] ?? '');
$body->rewind();
$response = new Response(
$body,
200,
$responseData['headers'] ?? [],
$responseData['reasonPhrase'] ?? ''
);
$responseData = [
'statuscode' => $response->getStatusCode(),
'headers' => $response->getHeaders(),
'body' => $response->getBody()->getContents(),
'reasonPhrase' => $response->getReasonPhrase(),
];
$this->cache->set($cacheIdentifier, $responseData, $cacheTags);
$lock->release();
} catch (ClientException $e) {
$response = $this->createGenericErrorResponse('External error page could not be retrieved. ' . $e->getMessage());
$responseData = [
'statuscode' => $response->getStatusCode(),
'headers' => $response->getHeaders(),
'body' => $response->getBody()->getContents(),
'reasonPhrase' => $response->getReasonPhrase(),
];
$this->cache->set($cacheIdentifier, $responseData, $cacheTags);
} catch (LockAcquireWouldBlockException $e) {
// Currently a lock is active, thus returning a generic error directly to avoid
// long wait times and thus consuming too much php worker processes. Caching is
// not done here, as we do not know if the error page can be retrieved or not.
$lock->release();
return $this->createGenericErrorResponse('Lock could not be acquired. ' . $e->getMessage());
} catch (\Throwable $e) {
// Any other error happened
$lock->release();
return $this->createGenericErrorResponse('Error page could not be retrieved' . $e->getMessage());
}
$lock->release();
return $this->createCachedPageRequestResponse($responseData);
}
protected function createGenericErrorResponse(string $message = ''): ResponseInterface
{
$content = GeneralUtility::makeInstance(ErrorPageController::class)->errorAction(
'Page Not Found',
$message ?: 'Error page is being generated',
AbstractMessage::ERROR,
0,
503
);
return new HtmlResponse($content, 503);
}
protected function createCachedPageRequestResponse(array $responseData): ResponseInterface
{
$body = new Stream('php://temp', 'wb+');
$body->write($responseData['body'] ?? '');
$body->rewind();
$response = new Response(
$body,
$responseData['statuscode'] ?? 200,
$responseData['headers'] ?? [],
$responseData['reasonPhrase'] ?? ''
);
return $response;
}
......@@ -215,7 +284,7 @@ class PageContentErrorHandler implements PageErrorHandlerInterface
$options = [];
if ((int)$GLOBALS['TYPO3_CONF_VARS']['HTTP']['timeout'] === 0) {
$options = [
'timeout' => 30,
'timeout' => 10,
];
}
return $options;
......
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