TYPO3 subject to Uncontrolled Recursion resulting in Denial of Service
Description
TYPO3 is an open source PHP based web content management system. In versions prior to 9.5.38, 10.4.33, 11.5.20, and 12.1.1, requesting invalid or non-existing resources via HTTP triggers the page error handler, which again could retrieve content to be shown as an error message from another page. This leads to a scenario in which the application is calling itself recursively - amplifying the impact of the initial attack until the limits of the web server are exceeded. This vulnerability is very similar, but not identical, to the one described in CVE-2021-21359. This issue is patched in versions 9.5.38 ELTS, 10.4.33, 11.5.20 or 12.1.1.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
TYPO3 CMS prior to versions 9.5.38, 10.4.33, 11.5.20, 12.1.1 vulnerable to denial-of-service attack via recursive error page handling.
Root
Cause
The vulnerability resides in TYPO3's error handling mechanism. When a request for an invalid or non-existing resource is made, the page error handler is invoked. If the error handler is configured to retrieve content from another page to display as an error message, it may perform a subrequest to fetch that content. Under certain conditions, the subrequest itself can trigger another error, causing the handler to recursively call itself. This creates a chain of nested requests that continues until web server resources are exhausted [1][2].
Exploitation
An attacker can exploit this by sending HTTP requests to a TYPO3 instance that trigger the page error handler with a configured error content source that loops back to the same or another error-producing page. No authentication is required; the attack can be carried out remotely by any user who can make HTTP requests to the vulnerable site [2]. The recursive nature amplifies the impact of a single malicious request.
Impact
Successful exploitation leads to a denial-of-service condition. The uncontrolled recursion consumes web server resources (CPU, memory, connection slots) until the server's limits are exceeded, potentially causing the site to become unresponsive. This can disrupt service availability for legitimate users [2].
Mitigation
The issue is patched in TYPO3 versions 9.5.38 ELTS, 10.4.33, 11.5.20, and 12.1.1. The patches introduce a locking mechanism to prevent concurrent subrequests for the same error cache identifier, aborting recursion by returning a generic error response if a lock cannot be acquired [1][4]. Users should upgrade immediately or apply the provided security patches.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
typo3/cms-corePackagist | >= 9.0.0, < 9.5.38 | 9.5.38 |
typo3/cms-corePackagist | >= 10.0.0, < 10.4.33 | 10.4.33 |
typo3/cms-corePackagist | >= 11.0.0, < 11.5.20 | 11.5.20 |
typo3/cmsPackagist | >= 10.0.0, < 10.4.33 | 10.4.33 |
typo3/cmsPackagist | >= 11.0.0, < 11.5.20 | 11.5.20 |
Affected products
4- osv-coords3 versions
>= 9.0.0, < 9.5.38+ 2 more
- (no CPE)range: >= 9.0.0, < 9.5.38
- (no CPE)range: >= 10.0.0, < 10.4.33
- (no CPE)range: >= 9.0.0, < 9.5.38
Patches
21e5f44417f03[SECURITY] Avoid DoS when generating Error pages
1 file changed · +100 −31
typo3/sysext/core/Classes/Error/PageErrorHandler/PageContentErrorHandler.php+100 −31 modified@@ -17,19 +17,25 @@ 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; 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 @@ public function handlePageError(ServerRequestInterface $request, string $message { 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 @@ public function handlePageError(ServerRequestInterface $request, string $message 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 @@ protected function stashEnvironment(callable $fetcher): ResponseInterface /** * 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 @@ protected function getSubRequestOptions(): array $options = []; if ((int)$GLOBALS['TYPO3_CONF_VARS']['HTTP']['timeout'] === 0) { $options = [ - 'timeout' => 30, + 'timeout' => 10, ]; } return $options;
73b46b6a6270[SECURITY] Avoid DoS when generating Error pages
1 file changed · +30 −1
typo3/sysext/core/Classes/Error/PageErrorHandler/PageContentErrorHandler.php+30 −1 modified@@ -21,10 +21,14 @@ use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Cache\CacheManager; use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException; +use TYPO3\CMS\Core\Controller\ErrorPageController; 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\Locking\Exception\LockAcquireWouldBlockException; +use TYPO3\CMS\Core\Locking\LockFactory; +use TYPO3\CMS\Core\Locking\LockingStrategyInterface; use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException; use TYPO3\CMS\Core\Site\Entity\Site; use TYPO3\CMS\Core\Site\Entity\SiteLanguage; @@ -86,9 +90,25 @@ public function handlePageError(ServerRequestInterface $request, string $message $cacheContent = $cache->get($cacheIdentifier); if (!$cacheContent && $resolvedUrl !== (string)$request->getUri()) { + $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(); + } + $subResponse = GeneralUtility::makeInstance(RequestFactory::class) ->request($resolvedUrl, 'GET', $this->getSubRequestOptions()); + $lock->release(); + } catch (LockAcquireWouldBlockException $_) { + $lock->release(); + return $this->createGenericErrorResponse(); } catch (\Exception $e) { throw new \RuntimeException('Error handler could not fetch error page "' . $resolvedUrl . '", reason: ' . $e->getMessage(), 1544172838); } @@ -124,6 +144,15 @@ public function handlePageError(ServerRequestInterface $request, string $message return new HtmlResponse($content, $this->statusCode); } + protected function createGenericErrorResponse(string $message = ''): ResponseInterface + { + $content = GeneralUtility::makeInstance(ErrorPageController::class)->errorAction( + 'Page Not Found', + $message ?: 'The page did not exist or was inaccessible. Error page is being generated' + ); + return new HtmlResponse($content, 503); + } + /** * Returns request options for the subrequest and ensures, that a reasoneable timeout is present * @@ -134,7 +163,7 @@ protected function getSubRequestOptions(): array $options = []; if ((int)$GLOBALS['TYPO3_CONF_VARS']['HTTP']['timeout'] === 0) { $options = [ - 'timeout' => 30 + 'timeout' => 10 ]; } return $options;
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-8c28-5mp7-v24hghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-23500ghsaADVISORY
- github.com/FriendsOfPHP/security-advisories/blob/master/typo3/cms/CVE-2022-23500.yamlghsaWEB
- github.com/TYPO3/typo3/commit/1e5f44417f031c9c5a9f9d09a6a841cf89aa7b7aghsaWEB
- github.com/TYPO3/typo3/commit/73b46b6a627093112cfca4b895a198ca5e1970b7ghsaWEB
- github.com/TYPO3/typo3/security/advisories/GHSA-8c28-5mp7-v24hghsax_refsource_CONFIRMWEB
- typo3.org/security/advisory/typo3-core-sa-2022-012ghsaWEB
News mentions
0No linked articles in our index yet.