Skip to content
Snippets Groups Projects
Commit 36b63a8c authored by Stefan Bürk's avatar Stefan Bürk
Browse files

[BUGFIX] Use Guzzle request to fetch external error page content

TYPO3 provides the ability to configure different error handlers for
specific (or all) error codes, as well as the error handler type
to use (core handler or custom).

The `\TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler`
allows to specify a target using the link handler. The default config
allows to define `LinkService::TYPE_PAGE` and `LinkService::TYPE_URL`.

After implementing and stabilizing the TYPO3 sub-request feature,
this particular error handler has been refactored to use an internal
sub-request to resolve the error page content: An external URL not
matching the same instance or having a page unavailable within
the TYPO3 instance will not return any content.

This change modifies the `PageContentErrorHandler` to send a Guzzle
request for an external URL instead of using internal sub-requests,
even if it would access the originating instance again. A limitation
is that the requested URL **must** return a HTTP status-code 200.

A custom request header is attached to this request. It is then
checked to avoid recurring errors or loops in the page-resolving
workflow.

Resolves: #103399
Related: #98396
Related: #94402
Releases: main, 12.4
Change-Id: If09158abd2aa9246bcb7a4fa41a0ad6e4a0f942c
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/83947


Reviewed-by: default avatarThomas Hohn <tho@gyldendal.dk>
Reviewed-by: default avatarStefan Bürk <stefan@buerk.tech>
Tested-by: default avatarBenni Mack <benni@typo3.org>
Tested-by: default avatarcore-ci <typo3@b13.com>
Tested-by: default avatarChristian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: default avatarChristian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: default avatarBenni Mack <benni@typo3.org>
Tested-by: default avatarStefan Bürk <stefan@buerk.tech>
parent ea143a3c
No related branches found
No related tags found
No related merge requests found
......@@ -17,10 +17,13 @@ declare(strict_types=1);
namespace TYPO3\CMS\Core\Error\PageErrorHandler;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Http\Client\GuzzleClientFactory;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Core\LinkHandling\LinkService;
......@@ -44,6 +47,8 @@ class PageContentErrorHandler implements PageErrorHandlerInterface
protected ResponseFactoryInterface $responseFactory;
protected SiteFinder $siteFinder;
protected LinkService $link;
protected RequestFactoryInterface $requestFactory;
protected GuzzleClientFactory $guzzleClientFactory;
/**
* PageContentErrorHandler constructor.
......@@ -63,6 +68,8 @@ class PageContentErrorHandler implements PageErrorHandlerInterface
$this->responseFactory = $container->get(ResponseFactoryInterface::class);
$this->siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
$this->link = $container->get(LinkService::class);
$this->requestFactory = $container->get(RequestFactoryInterface::class);
$this->guzzleClientFactory = $container->get(GuzzleClientFactory::class);
}
public function handlePageError(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface
......@@ -70,6 +77,7 @@ class PageContentErrorHandler implements PageErrorHandlerInterface
try {
$urlParams = $this->link->resolve($this->errorHandlerConfiguration['errorContentSource']);
$urlParams['pageuid'] = (int)($urlParams['pageuid'] ?? 0);
$urlType = $urlParams['type'] ?? LinkService::TYPE_UNKNOWN;
$resolvedUrl = $this->resolveUrl($request, $urlParams);
// avoid denial-of-service amplification scenario
......@@ -79,6 +87,11 @@ class PageContentErrorHandler implements PageErrorHandlerInterface
$this->statusCode
);
}
// External URL most likely pointing to additional hosts or pages not contained in the current instance,
// and using internal sub requests would never receive a valid page. Send an external request instead.
if ($urlType === LinkService::TYPE_URL) {
return $this->sendExternalRequest($resolvedUrl, $request);
}
// Create a sub-request 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'], $request));
......@@ -128,6 +141,50 @@ class PageContentErrorHandler implements PageErrorHandlerInterface
return $this->application->handle($request);
}
/**
* Sends an external request to fetch the error page from a remote resource.
*
* A custom header is added and checked to mitigate request loops, which
* indicates additional configuration error in the error handler config.
*/
protected function sendExternalRequest(string $url, ServerRequestInterface $originalRequest): ResponseInterface
{
if ($originalRequest->hasHeader('Requested-By')
&& in_array('TYPO3 Error Handler', $originalRequest->getHeader('Requested-By'), true)
) {
// If the header is set here, it is a recursive call within the same instance where an
// outer error handler called a page that results in another error handler call. To break
// the loop, we except here.
return new HtmlResponse(
'The error page could not be resolved, the error page itself is not accessible',
$this->statusCode
);
}
try {
$request = $this->requestFactory->createRequest('GET', $url)
->withHeader('Content-Type', 'text/html')
->withHeader('Requested-By', 'TYPO3 Error Handler');
$response = $this->guzzleClientFactory->getClient()->send($request);
// In case global guzzle configuration has been changed to not throw an exception
// for error status codes, the response status code is checked here.
if ($response->getStatusCode() >= 300) {
return new HtmlResponse(
'The error page could not be resolved, as the error page itself is not accessible',
$this->statusCode
);
}
return $this->responseFactory
->createResponse($this->statusCode)
->withHeader('Content-Type', $response->getHeader('Content-Type'))
->withBody($response->getBody());
} catch (GuzzleException) {
return new HtmlResponse(
'The error page could not be resolved, the error page itself is not accessible',
$this->statusCode
);
}
}
/**
* Resolve the URL (currently only page and external URL are supported)
*/
......
......@@ -19,6 +19,7 @@ namespace TYPO3\CMS\Core\Tests\Unit\Site\Entity;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Symfony\Component\DependencyInjection\Container;
use TYPO3\CMS\Core\Cache\CacheManager;
......@@ -190,7 +191,9 @@ final class SiteTest extends UnitTestCase
$container = new Container();
$container->set(Application::class, $app);
$container->set(Features::class, new Features());
$container->set(GuzzleClientFactory::class, new GuzzleClientFactory());
$container->set(RequestFactory::class, new RequestFactory(new GuzzleClientFactory()));
$container->set(RequestFactoryInterface::class, new RequestFactory(new GuzzleClientFactory()));
$container->set(ResponseFactoryInterface::class, new ResponseFactory());
$container->set(LinkService::class, $link);
$container->set(SiteFinder::class, $siteFinder);
......@@ -267,7 +270,9 @@ final class SiteTest extends UnitTestCase
$container = new Container();
$container->set(Application::class, $app);
$container->set(Features::class, new Features());
$container->set(GuzzleClientFactory::class, new GuzzleClientFactory());
$container->set(RequestFactory::class, new RequestFactory(new GuzzleClientFactory()));
$container->set(RequestFactoryInterface::class, new RequestFactory(new GuzzleClientFactory()));
$container->set(ResponseFactoryInterface::class, new ResponseFactory());
$container->set(LinkService::class, $link);
$container->set(SiteFinder::class, $siteFinder);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment