From 6c6f137e281208e8f81ef577c03f4f0ef3c82e4a Mon Sep 17 00:00:00 2001 From: Oliver Hader <oliver@typo3.org> Date: Wed, 7 Dec 2022 17:37:55 +0100 Subject: [PATCH] [TASK] Add HTTP host header injection check to reports module In case the web server scenario is not properly configured to deny HTTP host header injection, and the trustedHostsPattern is not explicit enough, a corresponding check in the reports module will issue an error message like * HTTP_HOST contained unexpected "a0a3aa2f59.random.example.org" * SERVER_NAME contained unexpected "a0a3aa2f59.random.example.org" Using the configuration directive `UseCanonicalName On` for Apache web server environments mitigates the risk. Resolves: #99347 Releases: main, 11.5, 10.4 Change-Id: Iaafd136fd817a0722f482d1d0e6b198382e40e3d Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/77025 Tested-by: Benni Mack <benni@typo3.org> Tested-by: Benjamin Franzke <bfr@qbus.de> Tested-by: core-ci <typo3@b13.com> Reviewed-by: Benjamin Franzke <bfr@qbus.de> Reviewed-by: Benni Mack <benni@typo3.org> --- .../Middleware/BackendUserAuthenticator.php | 1 + ...-IntegrateServerResponseSecurityChecks.rst | 5 ++ .../ServerResponseCheckController.php | 57 +++++++++++++++++++ .../ServerResponse/ServerResponseCheck.php | 45 ++++++++++++++- .../install/Configuration/Backend/Routes.php | 6 ++ 5 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 typo3/sysext/install/Classes/Controller/ServerResponseCheckController.php diff --git a/typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php b/typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php index 332aa43697a2..20f107823445 100644 --- a/typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php +++ b/typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php @@ -61,6 +61,7 @@ class BackendUserAuthenticator extends \TYPO3\CMS\Core\Middleware\BackendUserAut '/login/password-reset/initiate-reset', '/login/password-reset/validate', '/login/password-reset/finish', + '/install/server-response-check/host', '/ajax/login', '/ajax/logout', '/ajax/login/preflight', diff --git a/typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-91354-IntegrateServerResponseSecurityChecks.rst b/typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-91354-IntegrateServerResponseSecurityChecks.rst index 8dd71d5f2496..f5329708edee 100644 --- a/typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-91354-IntegrateServerResponseSecurityChecks.rst +++ b/typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-91354-IntegrateServerResponseSecurityChecks.rst @@ -22,6 +22,11 @@ It is evaluated whether non-standard file extensions lead to unexpected handling on the server-side, such as `test.php.wrong` being evaluated as PHP or `test.html.wrong` being served with `text/html` content type. +Besides that, HTTP host header injection is evaluated. In case `HTTP_HOST` or +`SERVER_NAME` were reported to contain unexpected values, this is an indicator +for being affected by this configuration flaw. For Apache web servers, using the +configuration directive `UseCanonicalName On` might solve this problem. + Details are explained in `TYPO3 Security Guidelines for Administrators`_. .. _TYPO3 Security Guidelines for Administrators: https://docs.typo3.org/m/typo3/reference-coreapi/10.4/en-us/Security/GuidelinesAdministrators/Index.html#file-extension-handling diff --git a/typo3/sysext/install/Classes/Controller/ServerResponseCheckController.php b/typo3/sysext/install/Classes/Controller/ServerResponseCheckController.php new file mode 100644 index 000000000000..28564ff724e9 --- /dev/null +++ b/typo3/sysext/install/Classes/Controller/ServerResponseCheckController.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Install\Controller; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Http\JsonResponse; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Used from backend `/typo3` context to check webserver response in general (independent of install tool). + * @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API. + */ +class ServerResponseCheckController +{ + public static function hmac(string $value): string + { + return GeneralUtility::hmac($value, ServerResponseCheckController::class); + } + + public function checkHostAction(ServerRequestInterface $request): ResponseInterface + { + $time = $request->getQueryParams()['src-time'] ?? null; + $hash = $request->getQueryParams()['src-hash'] ?? null; + + if (empty($time) || !is_string($time) || empty($hash) || !is_string($hash)) { + return new JsonResponse(['error' => 'Query params src-time` and src-hash` are required.'], 400); + } + if (!hash_equals(self::hmac($time), $hash)) { + return new JsonResponse(['error' => 'Invalid time or hash provided.'], 400); + } + if ((int)$time + 60 < time()) { + return new JsonResponse(['error' => 'Request expired.'], 400); + } + + return new JsonResponse([ + 'server.HTTP_HOST' => $_SERVER['HTTP_HOST'] ?? null, + 'server.SERVER_NAME' => $_SERVER['SERVER_NAME'] ?? null, + 'server.SERVER_PORT' => $_SERVER['SERVER_PORT'] ?? null, + ]); + } +} diff --git a/typo3/sysext/install/Classes/SystemEnvironment/ServerResponse/ServerResponseCheck.php b/typo3/sysext/install/Classes/SystemEnvironment/ServerResponse/ServerResponseCheck.php index 71bdeb736647..6aa216f55f78 100644 --- a/typo3/sysext/install/Classes/SystemEnvironment/ServerResponse/ServerResponseCheck.php +++ b/typo3/sysext/install/Classes/SystemEnvironment/ServerResponse/ServerResponseCheck.php @@ -23,9 +23,12 @@ use GuzzleHttp\Exception\BadResponseException; use function GuzzleHttp\Promise\settle; use Psr\Http\Message\ResponseInterface; +use TYPO3\CMS\Backend\Routing\UriBuilder; +use TYPO3\CMS\Core\Crypto\Random; use TYPO3\CMS\Core\Messaging\FlashMessage; use TYPO3\CMS\Core\Messaging\FlashMessageQueue; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Install\Controller\ServerResponseCheckController; use TYPO3\CMS\Install\SystemEnvironment\CheckInterface; use TYPO3\CMS\Reports\Status; @@ -99,7 +102,7 @@ class ServerResponseCheck implements CheckInterface $severity = Status::WARNING; } return new Status( - 'Server Response on static files', + 'Server Response', $title ?? 'OK', $this->wrapList($messages, $label ?? '', self::WRAP_NESTED), $severity ?? Status::OK @@ -121,6 +124,7 @@ class ServerResponseCheck implements CheckInterface } try { $this->buildFileDeclarations(); + $this->processHostCheck($messageQueue); $this->processFileDeclarations($messageQueue); $this->finishMessageQueue($messageQueue); } finally { @@ -207,6 +211,45 @@ class ServerResponseCheck implements CheckInterface GeneralUtility::rmdir($this->fileadminLocation->getFilePath(), true); } + protected function processHostCheck(FlashMessageQueue $messageQueue): void + { + $random = GeneralUtility::makeInstance(Random::class); + $randomHost = $random->generateRandomHexString(10) . '.random.example.org'; + $time = (string)time(); + $url = GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute( + 'install.server-response-check.host', + ['src-time' => $time, 'src-hash' => ServerResponseCheckController::hmac($time)], + UriBuilder::ABSOLUTE_URL + ); + try { + $client = new Client(['timeout' => 10]); + $response = $client->request('GET', (string)$url, ['headers' => ['Host' => $randomHost]]); + } catch (BadResponseException $exception) { + // it is expected that the previous request fails + return; + } + // in case we end up here, the server processed an HTTP request with invalid HTTP host header + $messageParts = []; + $data = json_decode((string)$response->getBody(), true); + $serverHttpHost = $data['server.HTTP_HOST'] ?? null; + $serverServerName = $data['server.SERVER_NAME'] ?? null; + if ($serverHttpHost === $randomHost) { + $messageParts[] = sprintf('HTTP_HOST contained unexpected "%s"', $randomHost); + } + if ($serverServerName === $randomHost) { + $messageParts[] = sprintf('SERVER_NAME contained unexpected "%s"', $randomHost); + } + if ($messageParts !== []) { + $messageQueue->addMessage( + new FlashMessage( + $this->wrapList($messageParts, (string)$url, self::WRAP_FLAT), + 'Unexpected server response', + FlashMessage::ERROR + ) + ); + } + } + protected function processFileDeclarations(FlashMessageQueue $messageQueue): void { $promises = []; diff --git a/typo3/sysext/install/Configuration/Backend/Routes.php b/typo3/sysext/install/Configuration/Backend/Routes.php index 34787af7efd9..87a66f98fbd1 100644 --- a/typo3/sysext/install/Configuration/Backend/Routes.php +++ b/typo3/sysext/install/Configuration/Backend/Routes.php @@ -1,6 +1,7 @@ <?php use TYPO3\CMS\Install\Controller\BackendModuleController; +use TYPO3\CMS\Install\Controller\ServerResponseCheckController; /** * Defines routes for Install Tool being called from backend context. @@ -11,4 +12,9 @@ return [ 'path' => '/install/backend-user-confirmation', 'target' => BackendModuleController::class . '::backendUserConfirmationAction', ], + 'install.server-response-check.host' => [ + 'access' => 'public', + 'path' => '/install/server-response-check/host', + 'target' => ServerResponseCheckController::class . '::checkHostAction', + ], ]; -- GitLab