VYPR
Moderate severityNVD Advisory· Published Dec 14, 2022· Updated Apr 21, 2025

TYPO3 subject to Uncontrolled Recursion resulting in Denial of Service

CVE-2022-23500

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.

PackageAffected versionsPatched versions
typo3/cms-corePackagist
>= 9.0.0, < 9.5.389.5.38
typo3/cms-corePackagist
>= 10.0.0, < 10.4.3310.4.33
typo3/cms-corePackagist
>= 11.0.0, < 11.5.2011.5.20
typo3/cmsPackagist
>= 10.0.0, < 10.4.3310.4.33
typo3/cmsPackagist
>= 11.0.0, < 11.5.2011.5.20

Affected products

4

Patches

2
1e5f44417f03

[SECURITY] Avoid DoS when generating Error pages

https://github.com/TYPO3/typo3Benni MackDec 13, 2022via ghsa
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

https://github.com/TYPO3/typo3Benni MackDec 13, 2022via ghsa
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

News mentions

0

No linked articles in our index yet.