diff --git a/typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php b/typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php index 332aa43697a243a68787b278d270d6193b2f78cc..20f107823445de34f6640deed8f18488f23bef53 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 8dd71d5f249673b18146a830ce23518a565ca81d..f5329708edee625bb31a055c621f286684fa30d8 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 0000000000000000000000000000000000000000..28564ff724e94a431476d7f27b911c9804859faf --- /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 71bdeb73664750c7d744e95d5f5e8b77434b78e0..6aa216f55f786c1206aca88233a990d6770dc50c 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 34787af7efd990cd5c797abfc591d2644611124b..87a66f98fbd17b181cb5828be7c966767116daae 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', + ], ];