Skip to content
Permalink
Browse files Browse the repository at this point in the history
[SECURITY] Verify HTTP_HOST via FE/BE middleware
Avoid a dependency cycle between HTTP_HOST generation
and verification.
As $GLOBALS['TYPO3_REQUEST'] is not available
during ServerRequestFactory::fromGlobals(), HTTP_HOST
verification can not be performed at that point.
It is therefore delayed into a context aware middleware
instead of being skipped because of missing $GLOBALS.

Positive advantage of moving the verification into
frontend and backend middlewares, is that context
checks to exclude CLI/installtool can be dropped.

As a side effect this also fixes the frontend to installtool
redirect if TYPO3 is not yet configured and running with
an invalid SERVER_NAME, as ServerRequestFactory::fromGlobals()
doesn't fail.

Releases: master
Resolves: #95395
Change-Id: Idd3a3449a878cd625dad0d04892d9f0e710ca1a9
Security-Bulletin: TYPO3-CORE-SA-2021-015
Security-References: CVE-2021-41114
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/71438
Tested-by: Oliver Hader <oliver.hader@typo3.org>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
  • Loading branch information
bnf authored and ohader committed Oct 5, 2021
1 parent fa51999 commit 5cbff85
Show file tree
Hide file tree
Showing 17 changed files with 531 additions and 387 deletions.
7 changes: 7 additions & 0 deletions typo3/sysext/backend/Configuration/RequestMiddlewares.php
Expand Up @@ -11,9 +11,16 @@
*/
return [
'backend' => [
/** internal: do not use or reference this middleware in your own code */
'typo3/cms-core/verify-host-header' => [
'target' => \TYPO3\CMS\Core\Middleware\VerifyHostHeader::class,
],
/** internal: do not use or reference this middleware in your own code */
'typo3/cms-core/normalized-params-attribute' => [
'target' => \TYPO3\CMS\Core\Middleware\NormalizedParamsAttribute::class,
'after' => [
'typo3/cms-core/verify-host-header',
],
],
'typo3/cms-backend/locked-backend' => [
'target' => \TYPO3\CMS\Backend\Middleware\LockedBackendGuard::class,
Expand Down
13 changes: 2 additions & 11 deletions typo3/sysext/core/Classes/Http/NormalizedParams.php
Expand Up @@ -548,13 +548,12 @@ public function getQueryString(): string
}

/**
* Sanitize HTTP_HOST, take proxy configuration into account and
* verify allowed hosts with configured trusted hosts pattern.
* Normalize HTTP_HOST by taking proxy configuration into account.
*
* @param array $serverParams Basically the $_SERVER, but from $request object
* @param array $configuration $TYPO3_CONF_VARS['SYS'] array
* @param bool $isBehindReverseProxy True if reverse proxy setup is detected
* @return string Sanitized HTTP_HOST
* @return string Normalized HTTP_HOST
*/
protected static function determineHttpHost(
array $serverParams,
Expand Down Expand Up @@ -585,14 +584,6 @@ protected static function determineHttpHost(
$httpHost = $xForwardedHost;
}
}
if (!GeneralUtility::isAllowedHostHeaderValue($httpHost)) {
throw new \UnexpectedValueException(
'The current host header value does not match the configured trusted hosts pattern!'
. ' Check the pattern defined in $GLOBALS[\'TYPO3_CONF_VARS\'][\'SYS\'][\'trustedHostsPattern\']'
. ' and adapt it, if you want to allow the current host header \'' . $httpHost . '\' for your installation.',
1396795886
);
}
return $httpHost;
}

Expand Down
126 changes: 126 additions & 0 deletions typo3/sysext/core/Classes/Middleware/VerifyHostHeader.php
@@ -0,0 +1,126 @@
<?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\Core\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
* Checks if the provided host header value matches the trusted hosts pattern.
*
* @internal
*/
class VerifyHostHeader implements MiddlewareInterface
{
public const ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL = '.*';
public const ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME = 'SERVER_NAME';

protected string $trustedHostsPattern;

public function __construct(string $trustedHostsPattern)
{
$this->trustedHostsPattern = $trustedHostsPattern;
}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$serverParams = $request->getServerParams();
$httpHost = $serverParams['HTTP_HOST'] ?? '';
if (!$this->isAllowedHostHeaderValue($httpHost, $serverParams)) {
throw new \UnexpectedValueException(
'The current host header value does not match the configured trusted hosts pattern!'
. ' Check the pattern defined in $GLOBALS[\'TYPO3_CONF_VARS\'][\'SYS\'][\'trustedHostsPattern\']'
. ' and adapt it, if you want to allow the current host header \'' . $httpHost . '\' for your installation.',
1396795884
);
}

return $handler->handle($request);
}

/**
* Checks if the provided host header value matches the trusted hosts pattern.
*
* @param string $hostHeaderValue HTTP_HOST header value as sent during the request (may include port)
* @return bool
*/
public function isAllowedHostHeaderValue(string $hostHeaderValue, array $serverParams): bool
{
// Deny the value if trusted host patterns is empty, which means configuration is invalid.
if ($this->trustedHostsPattern === '') {
return false;
}

if ($this->trustedHostsPattern === self::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL) {
return true;
}

return $this->hostHeaderValueMatchesTrustedHostsPattern($hostHeaderValue, $serverParams);
}

/**
* Checks if the provided host header value matches the trusted hosts pattern without any preprocessing.
*/
protected function hostHeaderValueMatchesTrustedHostsPattern(string $hostHeaderValue, array $serverParams): bool
{
if ($this->trustedHostsPattern === self::ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME) {
$host = strtolower($hostHeaderValue);
// Default port to be verified if HTTP_HOST does not contain explicit port information.
// Deriving from raw/local webserver HTTPS information (not taking possible proxy configurations into account)
// as we compare against the raw/local server information (SERVER_PORT).
$port = self::webserverUsesHttps($serverParams) ? '443' : '80';

$parsedHostValue = parse_url('http://' . $host);
if (isset($parsedHostValue['port'])) {
$host = $parsedHostValue['host'];
$port = (string)$parsedHostValue['port'];
}

// Allow values that equal the server name
// Note that this is only secure if name base virtual host are configured correctly in the webserver
$hostMatch = $host === strtolower($serverParams['SERVER_NAME']) && $port === $serverParams['SERVER_PORT'];
} else {
// In case name based virtual hosts are not possible, we allow setting a trusted host pattern
// See https://typo3.org/teams/security/security-bulletins/typo3-core/typo3-core-sa-2014-001/ for further details
$hostMatch = (bool)preg_match('/^' . $this->trustedHostsPattern . '$/i', $hostHeaderValue);
}

return $hostMatch;
}

/**
* Determine if the webserver uses HTTPS.
*
* HEADS UP: This does not check if the client performed a
* HTTPS request, as possible proxies are not taken into
* account. It provides raw information about the current
* webservers configuration only.
*/
protected function webserverUsesHttps(array $serverParams): bool
{
if (!empty($serverParams['SSL_SESSION_ID'])) {
return true;
}

// https://secure.php.net/manual/en/reserved.variables.server.php
// "Set to a non-empty value if the script was queried through the HTTPS protocol."
return !empty($serverParams['HTTPS']) && strtolower($serverParams['HTTPS']) !== 'off';
}
}
8 changes: 8 additions & 0 deletions typo3/sysext/core/Classes/ServiceProvider.php
Expand Up @@ -70,6 +70,7 @@ public function getFactories(): array
Mail\TransportFactory::class => [ static::class, 'getMailTransportFactory' ],
Messaging\FlashMessageService::class => [ static::class, 'getFlashMessageService' ],
Middleware\ResponsePropagation::class => [ static::class, 'getResponsePropagationMiddleware' ],
Middleware\VerifyHostHeader::class => [ static::class, 'getVerifyHostHeaderMiddleware' ],
Package\FailsafePackageManager::class => [ static::class, 'getFailsafePackageManager' ],
Package\Cache\PackageDependentCacheIdentifier::class => [ static::class, 'getPackageDependentCacheIdentifier' ],
Registry::class => [ static::class, 'getRegistry' ],
Expand Down Expand Up @@ -369,6 +370,13 @@ public static function getResponsePropagationMiddleware(ContainerInterface $cont
return self::new($container, Middleware\ResponsePropagation::class);
}

public static function getVerifyHostHeaderMiddleware(ContainerInterface $container): Middleware\VerifyHostHeader
{
return self::new($container, Middleware\VerifyHostHeader::class, [
$GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] ?? '',
]);
}

public static function getFailsafePackageManager(ContainerInterface $container): Package\FailsafePackageManager
{
$packageManager = $container->get(Package\PackageManager::class);
Expand Down
94 changes: 7 additions & 87 deletions typo3/sysext/core/Classes/Utility/GeneralUtility.php
Expand Up @@ -30,6 +30,7 @@
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\Http\RequestFactory;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Middleware\VerifyHostHeader;
use TYPO3\CMS\Core\Package\Exception as PackageException;
use TYPO3\CMS\Core\SingletonInterface;

Expand All @@ -46,17 +47,11 @@
*/
class GeneralUtility
{
/* @deprecated since v11, will be removed in v12. */
const ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL = '.*';
/* @deprecated since v11, will be removed in v12. */
const ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME = 'SERVER_NAME';

/**
* State of host header value security check
* in order to avoid unnecessary multiple checks during one request
*
* @var bool
*/
protected static $allowHostHeaderValue = false;

/**
* @var ContainerInterface|null
*/
Expand Down Expand Up @@ -2563,12 +2558,6 @@ public static function getIndpEnv($getEnvName)
$retVal = $host;
}
}
if (!static::isAllowedHostHeaderValue($retVal)) {
throw new \UnexpectedValueException(
'The current host header value does not match the configured trusted hosts pattern! Check the pattern defined in $GLOBALS[\'TYPO3_CONF_VARS\'][\'SYS\'][\'trustedHostsPattern\'] and adapt it, if you want to allow the current host header \'' . $retVal . '\' for your installation.',
1396795884
);
}
break;
case 'HTTP_REFERER':

Expand Down Expand Up @@ -2695,68 +2684,17 @@ public static function getIndpEnv($getEnvName)

/**
* Checks if the provided host header value matches the trusted hosts pattern.
* If the pattern is not defined (which only can happen early in the bootstrap), deny any value.
* The result is saved, so the check needs to be executed only once.
*
* @param string $hostHeaderValue HTTP_HOST header value as sent during the request (may include port)
* @return bool
* @deprecated will be removed in TYPO3 v12.0.
*/
public static function isAllowedHostHeaderValue($hostHeaderValue)
{
if (static::$allowHostHeaderValue === true) {
return true;
}

if (static::isInternalRequestType()) {
return static::$allowHostHeaderValue = true;
}

// Deny the value if trusted host patterns is empty, which means we are early in the bootstrap
if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'])) {
return false;
}

if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] === self::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL) {
static::$allowHostHeaderValue = true;
} else {
static::$allowHostHeaderValue = static::hostHeaderValueMatchesTrustedHostsPattern($hostHeaderValue);
}

return static::$allowHostHeaderValue;
}

/**
* Checks if the provided host header value matches the trusted hosts pattern without any preprocessing.
*
* @param string $hostHeaderValue
* @return bool
* @internal
*/
public static function hostHeaderValueMatchesTrustedHostsPattern($hostHeaderValue)
{
if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] === self::ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME) {
$host = strtolower($hostHeaderValue);
// Default port to be verified if HTTP_HOST does not contain explicit port information.
// Deriving from raw/local webserver HTTPS information (not taking possible proxy configurations into account)
// as we compare against the raw/local server information (SERVER_PORT).
$port = self::webserverUsesHttps() ? '443' : '80';

$parsedHostValue = parse_url('http://' . $host);
if (isset($parsedHostValue['port'])) {
$host = $parsedHostValue['host'];
$port = (string)$parsedHostValue['port'];
}

// Allow values that equal the server name
// Note that this is only secure if name base virtual host are configured correctly in the webserver
$hostMatch = $host === strtolower($_SERVER['SERVER_NAME']) && $port === $_SERVER['SERVER_PORT'];
} else {
// In case name based virtual hosts are not possible, we allow setting a trusted host pattern
// See https://typo3.org/teams/security/security-bulletins/typo3-core/typo3-core-sa-2014-001/ for further details
$hostMatch = (bool)preg_match('/^' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] . '$/i', $hostHeaderValue);
}
trigger_error('GeneralUtility::isAllowedHostHeaderValue() will be removed in TYPO3 v12.0. Host header is verified by frontend and backend middlewares.', E_USER_DEPRECATED);

return $hostMatch;
$verifyHostHeader = new VerifyHostHeader($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] ?? '');
return $verifyHostHeader->isAllowedHostHeaderValue($hostHeaderValue, $_SERVER);
}

/**
Expand All @@ -2780,24 +2718,6 @@ protected static function webserverUsesHttps()
return !empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off';
}

/**
* Allows internal requests to the install tool and from the command line.
* We accept this risk to have the install tool always available.
* Also CLI needs to be allowed as unfortunately AbstractUserAuthentication::getAuthInfoArray()
* accesses HTTP_HOST without reason on CLI
* Additionally, allows requests when no REQUESTTYPE is set, which can happen quite early in the
* Bootstrap. See Application.php in EXT:backend/Classes/Http/.
*
* @return bool
*/
protected static function isInternalRequestType()
{
return Environment::isCli()
|| !isset($GLOBALS['TYPO3_REQUEST'])
|| !($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface)
|| (bool)((int)($GLOBALS['TYPO3_REQUEST'])->getAttribute('applicationType') & TYPO3_REQUESTTYPE_INSTALL);
}

/*************************
*
* TYPO3 SPECIFIC FUNCTIONS
Expand Down
@@ -0,0 +1,53 @@
.. include:: ../../Includes.txt

====================================================================================================
Deprecation: #95395 - GeneralUtility::isAllowedHostHeaderValue() and TRUSTED_HOSTS_PATTERN constants
====================================================================================================

See :issue:`95395`

Description
===========

The PHP method
:php:`TYPO3\CMS\Core\Utility\GeneralUtility::isAllowedHostHeaderValue()`
and the PHP constants
:php:`TYPO3\CMS\Core\Utility\GeneralUtility::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL`
and
:php:`TYPO3\CMS\Core\Utility\GeneralUtility::ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME`
have been deprecated.


Impact
======

A deprecation will be logged in TYPO3 v11 if
:php:`TYPO3\CMS\Core\Utility\GeneralUtility::isAllowedHostHeaderValue()` is
used. It is unlikely for extensions to have used this as the host header
is checked for every frontend and backend request anyway.

Usage of the constants will cause a PHP error "Undefined class constant" in
TYPO3 v12, the method
:php:`TYPO3\CMS\Core\Utility\GeneralUtility::isAllowedHostHeaderValue()` will be
dropped without replacement.


Affected Installations
======================

Installations using the constants instead of static strings or
call the method explictily – which is unlikely.


Migration
=========

Use :php:`'.*'` instead of
:php:`TYPO3\CMS\Core\Utility\GeneralUtility::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL`
and :php:`'SERVER_NAME'` instead of
:php:`TYPO3\CMS\Core\Utility\GeneralUtility::ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME`.

Don't use :php:`TYPO3\CMS\Core\Utility\GeneralUtility::isAllowedHostHeaderValue()`.


.. index:: PHP-API, FullyScanned, ext:core
Expand Up @@ -85,8 +85,7 @@ public function seeViewUpgradeDocumentation(ApplicationTester $I, ModalDialog $m
$I->click($versionPanel . ' a[data-bs-toggle="collapse"]');
$I->click($versionPanel . ' .t3js-upgradeDocs-markRead');

$textNewFirstPanelHeading = $I->grabTextFrom($versionPanel . ' .panel-heading');
$I->assertNotEquals($textCurrentFirstPanelHeading, $textNewFirstPanelHeading);
$I->dontSee($textCurrentFirstPanelHeading, '#version-1');

$I->amGoingTo('mark an item as unread');
$I->click('#heading-read');
Expand Down

0 comments on commit 5cbff85

Please sign in to comment.