CVE-2021-41114 (PHP, published 2021-10-05T18:15Z)
Description: value: TYPO3 is an open source PHP based web content management system released under the GNU GPL. It has been discovered that TYPO3 CMS is susceptible to host spoofing due to improper validation of the HTTP Host header. TYPO3 uses the HTTP Host header, for example, to generate absolute URLs during the frontend rendering process. Since the host header itself is provided by the client, it can be forged to any value, even in a name-based virtual hosts environment. This vulnerability is the same as described in TYPO3-CORE-SA-2014-001 (CVE-2014-3941). A regression, introduced during TYPO3 v11 development, led to this situation. The already existing setting $GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] (used as an effective mitigation strategy in previous TYPO3 versions) was not evaluated anymore, and reintroduced the vulnerability.
Severity: MEDIUM (CVSS v3 base score: 5.3)
Weakness: CWE-20 \u2014 Improper Input Validation
The product receives input or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.
Repository: https://github.com/TYPO3/typo3
Files changed:
typo3/sysext/backend/Configuration/RequestMiddlewares.php
typo3/sysext/core/Classes/Http/NormalizedParams.php
typo3/sysext/core/Classes/Middleware/VerifyHostHeader.php
typo3/sysext/core/Classes/ServiceProvider.php
typo3/sysext/core/Classes/Utility/GeneralUtility.php
typo3/sysext/core/Documentation/Changelog/11.5/Deprecation-95395-GeneralUtilityIsAllowedHostHeaderValueAndTrustedHostsPatternConstants.rst
typo3/sysext/core/Tests/Acceptance/Application/InstallTool/UpgradeCest.php
typo3/sysext/core/Tests/Unit/Middleware/VerifyHostHeaderTest.php
typo3/sysext/core/Tests/Unit/Utility/Fixtures/GeneralUtilityFixture.php
typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php
typo3/sysext/frontend/Configuration/RequestMiddlewares.php
typo3/sysext/install/Classes/Controller/InstallerController.php
typo3/sysext/install/Classes/ServiceProvider.php
typo3/sysext/install/Classes/SystemEnvironment/SetupCheck.php
typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassConstantMatcher.php
typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php
typo3/sysext/reports/Classes/Report/Status/SecurityStatus.php
Commit message: [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>


Vulnerable code:
     * Sanitize HTTP_HOST, take proxy configuration into account and
     * verify allowed hosts with configured trusted hosts pattern.
     * @return string Sanitized HTTP_HOST
        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
            );
        }
    /**
     * State of host header value security check
     * in order to avoid unnecessary multiple checks during one request
     *
     * @var bool
     */
    protected static $allowHostHeaderValue = false;

                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
                    );
                }
     * 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.
        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);
        }
        return $hostMatch;
    /**
     * 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);
    }

        $textNewFirstPanelHeading = $I->grabTextFrom($versionPanel . ' .panel-heading');
        $I->assertNotEquals($textCurrentFirstPanelHeading, $textNewFirstPanelHeading);
    public static int $isAllowedHostHeaderValueCallCount = 0;

    /**
     * Tracks number of calls done to this method
     *
     * @param string $hostHeaderValue Host name without port
     * @return bool
     */
    public static function isAllowedHostHeaderValue($hostHeaderValue): bool
    {
        self::$isAllowedHostHeaderValueCallCount++;
        return parent::isAllowedHostHeaderValue($hostHeaderValue);
    }

    /**
     * @param bool $allowHostHeaderValue
     */
    public static function setAllowHostHeaderValue(bool $allowHostHeaderValue): void
    {
        static::$allowHostHeaderValue = $allowHostHeaderValue;
    }

    /**
     * For testing we must not generally allow HTTP Host headers
     *
     * @return bool
     */
    protected static function isInternalRequestType(): bool
    {
        return false;
    }

        GeneralUtilityFixture::$isAllowedHostHeaderValueCallCount = 0;
        GeneralUtilityFixture::setAllowHostHeaderValue(false);
        $GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] = GeneralUtility::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL;
    /**
     * @test
     */
    public function isAllowedHostHeaderValueReturnsFalseIfTrustedHostsIsNotConfigured(): void
    {
        unset($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern']);
        self::assertFalse(GeneralUtilityFixture::isAllowedHostHeaderValue('evil.foo.bar'));
    }

    /**
     * @return array
     */
    public static function hostnamesMatchingTrustedHostsConfigurationDataProvider(): array
    {
        return [
            'hostname without port matching' => ['lolli.did.this', '.*\.did\.this'],
            'other hostname without port matching' => ['helmut.did.this', '.*\.did\.this'],
            'two different hostnames without port matching 1st host' => ['helmut.is.secure', '(helmut\.is\.secure|lolli\.is\.secure)'],
            'two different hostnames without port matching 2nd host' => ['lolli.is.secure', '(helmut\.is\.secure|lolli\.is\.secure)'],
            'hostname with port matching' => ['lolli.did.this:42', '.*\.did\.this:42'],
            'hostnames are case insensitive 1' => ['lolli.DID.this:42', '.*\.did.this:42'],
            'hostnames are case insensitive 2' => ['lolli.did.this:42', '.*\.DID.this:42'],
        ];
    }

    /**
     * @return array
     */
    public static function hostnamesNotMatchingTrustedHostsConfigurationDataProvider(): array
    {
        return [
            'hostname without port' => ['lolli.did.this', 'helmut\.did\.this'],
            'hostname with port, but port not allowed' => ['lolli.did.this:42', 'helmut\.did\.this'],
            'two different hostnames in pattern but host header starts with different value #1' => ['sub.helmut.is.secure', '(helmut\.is\.secure|lolli\.is\.secure)'],
            'two different hostnames in pattern but host header starts with different value #2' => ['sub.lolli.is.secure', '(helmut\.is\.secure|lolli\.is\.secure)'],
            'two different hostnames in pattern but host header ends with different value #1' => ['helmut.is.secure.tld', '(helmut\.is\.secure|lolli\.is\.secure)'],
            'two different hostnames in pattern but host header ends with different value #2' => ['lolli.is.secure.tld', '(helmut\.is\.secure|lolli\.is\.secure)'],
        ];
    }

    /**
     * @param string $httpHost HTTP_HOST string
     * @param string $hostNamePattern trusted hosts pattern
     * @test
     * @dataProvider hostnamesMatchingTrustedHostsConfigurationDataProvider
     */
    public function isAllowedHostHeaderValueReturnsTrueIfHostValueMatches(string $httpHost, string $hostNamePattern): void
    {
        $GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] = $hostNamePattern;
        self::assertTrue(GeneralUtilityFixture::isAllowedHostHeaderValue($httpHost));
    }

    /**
     * @param string $httpHost HTTP_HOST string
     * @param string $hostNamePattern trusted hosts pattern
     * @test
     * @dataProvider hostnamesNotMatchingTrustedHostsConfigurationDataProvider
     */
    public function isAllowedHostHeaderValueReturnsFalseIfHostValueMatches(string $httpHost, string $hostNamePattern): void
    {
        $GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] = $hostNamePattern;
        self::assertFalse(GeneralUtilityFixture::isAllowedHostHeaderValue($httpHost));
    }

    public function serverNamePatternDataProvider(): array
    {
        return [
            'host value matches server name and server port is default http' => [
                'httpHost' => 'secure.web.server',
                'serverName' => 'secure.web.server',
                'isAllowed' => true,
                'serverPort' => '80',
                'ssl' => 'Off',
            ],
            'host value matches server name if compared case insensitive 1' => [
                'httpHost' => 'secure.web.server',
                'serverName' => 'secure.WEB.server',
                'isAllowed' => true,
            ],
            'host value matches server name if compared case insensitive 2' => [
                'httpHost' => 'secure.WEB.server',
                'serverName' => 'secure.web.server',
                'isAllowed' => true,
            ],
            'host value matches server name and server port is default https' => [
                'httpHost' => 'secure.web.server',
                'serverName' => 'secure.web.server',
                'isAllowed' => true,
                'serverPort' => '443',
                'ssl' => 'On',
            ],
            'host value matches server name and server port' => [
                'httpHost' => 'secure.web.server:88',
                'serverName' => 'secure.web.server',
                'isAllowed' => true,
                'serverPort' => '88',
            ],
            'host value matches server name case insensitive 1 and server port' => [
                'httpHost' => 'secure.WEB.server:88',
                'serverName' => 'secure.web.server',
                'isAllowed' => true,
                'serverPort' => '88',
            ],
            'host value matches server name case insensitive 2 and server port' => [
                'httpHost' => 'secure.web.server:88',
                'serverName' => 'secure.WEB.server',
                'isAllowed' => true,
                'serverPort' => '88',
            ],
            'host value is ipv6 but matches server name and server port' => [
                'httpHost' => '[::1]:81',
                'serverName' => '[::1]',
                'isAllowed' => true,
                'serverPort' => '81',
            ],
            'host value does not match server name' => [
                'httpHost' => 'insecure.web.server',
                'serverName' => 'secure.web.server',
                'isAllowed' => false,
            ],
            'host value does not match server port' => [
                'httpHost' => 'secure.web.server:88',
                'serverName' => 'secure.web.server',
                'isAllowed' => false,
                'serverPort' => '89',
            ],
            'host value has default port that does not match server port' => [
                'httpHost' => 'secure.web.server',
                'serverName' => 'secure.web.server',
                'isAllowed' => false,
                'serverPort' => '81',
                'ssl' => 'Off',
            ],
            'host value has default port that does not match server ssl port' => [
                'httpHost' => 'secure.web.server',
                'serverName' => 'secure.web.server',
                'isAllowed' => false,
                'serverPort' => '444',
                'ssl' => 'On',
            ],
        ];
    }

    /**
     * @param string $httpHost
     * @param string $serverName
     * @param bool $isAllowed
     * @param string $serverPort
     * @param string $ssl
     *
     * @test
     * @dataProvider serverNamePatternDataProvider
     */
    public function isAllowedHostHeaderValueWorksCorrectlyWithWithServerNamePattern(
        string $httpHost,
        string $serverName,
        bool $isAllowed,
        string $serverPort = '80',
        string $ssl = 'Off'
    ): void {
        $GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] = GeneralUtility::ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME;
        $_SERVER['SERVER_NAME'] = $serverName;
        $_SERVER['SERVER_PORT'] = $serverPort;
        $_SERVER['HTTPS'] = $ssl;
        self::assertSame($isAllowed, GeneralUtilityFixture::isAllowedHostHeaderValue($httpHost));
    }

    /**
     * @param string $httpHost
     * @param string $serverName
     * @param bool $isAllowed
     * @param string $serverPort
     * @param string $ssl
     *
     * @test
     * @dataProvider serverNamePatternDataProvider
     */
    public function isAllowedHostHeaderValueWorksCorrectlyWithWithServerNamePatternAndSslProxy(
        string $httpHost,
        string $serverName,
        bool $isAllowed,
        string $serverPort = '80',
        string $ssl = 'Off'
    ): void {
        $backup = ['sys' => $GLOBALS['TYPO3_CONF_VARS']['SYS'], 'server' => $_SERVER];

        $GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] = GeneralUtility::ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME;
        $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxySSL'] = '*';
        $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] = '10.0.0.1';

        $_SERVER['REMOTE_ADDR'] = '10.0.0.1';
        $_SERVER['SERVER_NAME'] = $serverName;
        $_SERVER['SERVER_PORT'] = $serverPort;
        $_SERVER['HTTPS'] = $ssl;

        self::assertSame($isAllowed, GeneralUtilityFixture::isAllowedHostHeaderValue($httpHost));

        $GLOBALS['TYPO3_CONF_VARS']['SYS'] = $backup['sys'];
        $_SERVER = $backup['server'];
    }

    /**
     * @test
     */
    public function allGetIndpEnvCallsRelatedToHostNamesCallIsAllowedHostHeaderValue(): void
    {
        GeneralUtilityFixture::getIndpEnv('HTTP_HOST');
        GeneralUtility::flushInternalRuntimeCaches();
        GeneralUtilityFixture::getIndpEnv('TYPO3_HOST_ONLY');
        GeneralUtility::flushInternalRuntimeCaches();
        GeneralUtilityFixture::getIndpEnv('TYPO3_REQUEST_HOST');
        GeneralUtility::flushInternalRuntimeCaches();
        GeneralUtilityFixture::getIndpEnv('TYPO3_REQUEST_URL');
        self::assertSame(4, GeneralUtilityFixture::$isAllowedHostHeaderValueCallCount);
    }

    /**
     * @param string $httpHost HTTP_HOST string
     * @param string $hostNamePattern trusted hosts pattern
     * @test
     * @dataProvider hostnamesNotMatchingTrustedHostsConfigurationDataProvider
     */
    public function getIndpEnvForHostThrowsExceptionForNotAllowedHostnameValues(string $httpHost, string $hostNamePattern): void
    {
        $this->expectException(\UnexpectedValueException::class);
        $this->expectExceptionCode(1396795884);
        $_SERVER['HTTP_HOST'] = $httpHost;
        $GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] = $hostNamePattern;
        GeneralUtilityFixture::getIndpEnv('HTTP_HOST');
    }

    /**
     * @param string $httpHost HTTP_HOST string
     * @param string $hostNamePattern trusted hosts pattern (not used in this test currently)
     * @test
     * @dataProvider hostnamesNotMatchingTrustedHostsConfigurationDataProvider
     */
    public function getIndpEnvForHostAllowsAllHostnameValuesIfHostPatternIsSetToAllowAll(string $httpHost, string $hostNamePattern): void
    {
        $_SERVER['HTTP_HOST'] = $httpHost;
        $GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] = GeneralUtility::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL;
        self::assertSame($httpHost, GeneralUtility::getIndpEnv('HTTP_HOST'));
    }

                'typo3/cms-frontend/timetracker',
    public function checkTrustedHostsPatternAction(): ResponseInterface
            'success' => GeneralUtility::hostHeaderValueMatchesTrustedHostsPattern($_SERVER['HTTP_HOST']),
    public function executeAdjustTrustedHostsPatternAction(): ResponseInterface
        if (!GeneralUtility::hostHeaderValueMatchesTrustedHostsPattern($_SERVER['HTTP_HOST'])) {
        if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] === GeneralUtility::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL) {
            if (GeneralUtility::hostHeaderValueMatchesTrustedHostsPattern($_SERVER['HTTP_HOST'])) {
        if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] === GeneralUtility::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL) {

Fixed code:
        /** 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,
        ],
            'after' => [
                'typo3/cms-core/verify-host-header',
            ],
     * Normalize HTTP_HOST by taking proxy configuration into account.
     * @return string Normalized HTTP_HOST
<?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';
    }
}
            Middleware\VerifyHostHeader::class => [ static::class, 'getVerifyHostHeaderMiddleware' ],
    public static function getVerifyHostHeaderMiddleware(ContainerInterface $container): Middleware\VerifyHostHeader
    {
        return self::new($container, Middleware\VerifyHostHeader::class, [
            $GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] ?? '',
        ]);
    }

use TYPO3\CMS\Core\Middleware\VerifyHostHeader;
    /* @deprecated since v11, will be removed in v12. */
    /* @deprecated since v11, will be removed in v12. */
     * @deprecated will be removed in TYPO3 v12.0.
        trigger_error('GeneralUtility::isAllowedHostHeaderValue() will be removed in TYPO3 v12.0. Host header is verified by frontend and backend middlewares.', E_USER_DEPRECATED);
        $verifyHostHeader = new VerifyHostHeader($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] ?? '');
        return $verifyHostHeader->isAllowedHostHeaderValue($hostHeaderValue, $_SERVER);
.. 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
        $I->dontSee($textCurrentFirstPanelHeading, '#version-1');
<?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\Tests\Unit\Middleware;

use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Middleware\VerifyHostHeader;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;

class VerifyHostHeaderTest extends UnitTestCase
{
    use ProphecyTrait;

    /**
     * @test
     */
    public function isAllowedHostHeaderValueReturnsFalseIfTrustedHostsIsNotConfigured(): void
    {
        $subject = new VerifyHostHeader('');
        $serverParams = $_SERVER;
        self::assertFalse($subject->isAllowedHostHeaderValue('evil.foo.bar', $serverParams));
    }

    public static function hostnamesMatchingTrustedHostsConfigurationDataProvider(): array
    {
        return [
            'hostname without port matching' => ['lolli.did.this', '.*\.did\.this'],
            'other hostname without port matching' => ['helmut.did.this', '.*\.did\.this'],
            'two different hostnames without port matching 1st host' => ['helmut.is.secure', '(helmut\.is\.secure|lolli\.is\.secure)'],
            'two different hostnames without port matching 2nd host' => ['lolli.is.secure', '(helmut\.is\.secure|lolli\.is\.secure)'],
            'hostname with port matching' => ['lolli.did.this:42', '.*\.did\.this:42'],
            'hostnames are case insensitive 1' => ['lolli.DID.this:42', '.*\.did.this:42'],
            'hostnames are case insensitive 2' => ['lolli.did.this:42', '.*\.DID.this:42'],
        ];
    }

    public static function hostnamesNotMatchingTrustedHostsConfigurationDataProvider(): array
    {
        return [
            'hostname without port' => ['lolli.did.this', 'helmut\.did\.this'],
            'hostname with port, but port not allowed' => ['lolli.did.this:42', 'helmut\.did\.this'],
            'two different hostnames in pattern but host header starts with different value #1' => ['sub.helmut.is.secure', '(helmut\.is\.secure|lolli\.is\.secure)'],
            'two different hostnames in pattern but host header starts with different value #2' => ['sub.lolli.is.secure', '(helmut\.is\.secure|lolli\.is\.secure)'],
            'two different hostnames in pattern but host header ends with different value #1' => ['helmut.is.secure.tld', '(helmut\.is\.secure|lolli\.is\.secure)'],
            'two different hostnames in pattern but host header ends with different value #2' => ['lolli.is.secure.tld', '(helmut\.is\.secure|lolli\.is\.secure)'],
        ];
    }

    /**
     * @param string $httpHost HTTP_HOST string
     * @param string $hostNamePattern trusted hosts pattern
     * @test
     * @dataProvider hostnamesMatchingTrustedHostsConfigurationDataProvider
     */
    public function isAllowedHostHeaderValueReturnsTrueIfHostValueMatches(string $httpHost, string $hostNamePattern): void
    {
        $serverParams = $_SERVER;

        $subject = new VerifyHostHeader($hostNamePattern);
        self::assertTrue($subject->isAllowedHostHeaderValue($httpHost, $serverParams));
    }

    /**
     * @param string $httpHost HTTP_HOST string
     * @param string $hostNamePattern trusted hosts pattern
     * @test
     * @dataProvider hostnamesNotMatchingTrustedHostsConfigurationDataProvider
     */
    public function isAllowedHostHeaderValueReturnsFalseIfHostValueMatches(string $httpHost, string $hostNamePattern): void
    {
        $serverParams = $_SERVER;

        $subject = new VerifyHostHeader($hostNamePattern);
        self::assertFalse($subject->isAllowedHostHeaderValue($httpHost, $serverParams));
    }

    public function serverNamePatternDataProvider(): array
    {
        return [
            'host value matches server name and server port is default http' => [
                'httpHost' => 'secure.web.server',
                'serverName' => 'secure.web.server',
                'isAllowed' => true,
                'serverPort' => '80',
                'ssl' => 'Off',
            ],
            'host value matches server name if compared case insensitive 1' => [
                'httpHost' => 'secure.web.server',
                'serverName' => 'secure.WEB.server',
                'isAllowed' => true,
            ],
            'host value matches server name if compared case insensitive 2' => [
                'httpHost' => 'secure.WEB.server',
                'serverName' => 'secure.web.server',
                'isAllowed' => true,
            ],
            'host value matches server name and server port is default https' => [
                'httpHost' => 'secure.web.server',
                'serverName' => 'secure.web.server',
                'isAllowed' => true,
                'serverPort' => '443',
                'ssl' => 'On',
            ],
            'host value matches server name and server port' => [
                'httpHost' => 'secure.web.server:88',
                'serverName' => 'secure.web.server',
                'isAllowed' => true,
                'serverPort' => '88',
            ],
            'host value matches server name case insensitive 1 and server port' => [
                'httpHost' => 'secure.WEB.server:88',
                'serverName' => 'secure.web.server',
                'isAllowed' => true,
                'serverPort' => '88',
            ],
            'host value matches server name case insensitive 2 and server port' => [
                'httpHost' => 'secure.web.server:88',
                'serverName' => 'secure.WEB.server',
                'isAllowed' => true,
                'serverPort' => '88',
            ],
            'host value is ipv6 but matches server name and server port' => [
                'httpHost' => '[::1]:81',
                'serverName' => '[::1]',
                'isAllowed' => true,
                'serverPort' => '81',
            ],
            'host value does not match server name' => [
                'httpHost' => 'insecure.web.server',
                'serverName' => 'secure.web.server',
                'isAllowed' => false,
            ],
            'host value does not match server port' => [
                'httpHost' => 'secure.web.server:88',
                'serverName' => 'secure.web.server',
                'isAllowed' => false,
                'serverPort' => '89',
            ],
            'host value has default port that does not match server port' => [
                'httpHost' => 'secure.web.server',
                'serverName' => 'secure.web.server',
                'isAllowed' => false,
                'serverPort' => '81',
                'ssl' => 'Off',
            ],
            'host value has default port that does not match server ssl port' => [
                'httpHost' => 'secure.web.server',
                'serverName' => 'secure.web.server',
                'isAllowed' => false,
                'serverPort' => '444',
                'ssl' => 'On',
            ],
        ];
    }

    /**
     * @param string $httpHost
     * @param string $serverName
     * @param bool $isAllowed
     * @param string $serverPort
     * @param string $ssl
     *
     * @test
     * @dataProvider serverNamePatternDataProvider
     */
    public function isAllowedHostHeaderValueWorksCorrectlyWithWithServerNamePattern(
        string $httpHost,
        string $serverName,
        bool $isAllowed,
        string $serverPort = '80',
        string $ssl = 'Off'
    ): void {
        $serverParams = $_SERVER;
        $serverParams['SERVER_NAME'] = $serverName;
        $serverParams['SERVER_PORT'] = $serverPort;
        $serverParams['HTTPS'] = $ssl;

        $subject = new VerifyHostHeader(VerifyHostHeader::ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME);

        self::assertSame($isAllowed, $subject->isAllowedHostHeaderValue($httpHost, $serverParams));
    }

    /**
     * @param string $httpHost
     * @param string $serverName
     * @param bool $isAllowed
     * @param string $serverPort
     * @param string $ssl
     *
     * @test
     * @dataProvider serverNamePatternDataProvider
     */
    public function isAllowedHostHeaderValueWorksCorrectlyWithWithServerNamePatternAndSslProxy(
        string $httpHost,
        string $serverName,
        bool $isAllowed,
        string $serverPort = '80',
        string $ssl = 'Off'
    ): void {
        $serverParams = $_SERVER;
        $serverParams['REMOTE_ADDR'] = '10.0.0.1';
        $serverParams['SERVER_NAME'] = $serverName;
        $serverParams['SERVER_PORT'] = $serverPort;
        $serverParams['HTTPS'] = $ssl;

        $subject = new VerifyHostHeader(VerifyHostHeader::ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME);

        self::assertSame($isAllowed, $subject->isAllowedHostHeaderValue($httpHost, $serverParams));
    }

    /**
     * @param string $httpHost HTTP_HOST string
     * @param string $hostNamePattern trusted hosts pattern
     * @test
     * @dataProvider hostnamesNotMatchingTrustedHostsConfigurationDataProvider
     */
    public function processThrowsExceptionForNotAllowedHostnameValues(string $httpHost, string $hostNamePattern): void
    {
        $this->expectException(\UnexpectedValueException::class);
        $this->expectExceptionCode(1396795884);

        $serverParams = $_SERVER;
        $serverParams['HTTP_HOST'] = $httpHost;

        $subject = new VerifyHostHeader($hostNamePattern);

        $requestProphecy = $this->prophesize(ServerRequestInterface::class);
        $requestProphecy->getServerParams()->willReturn($serverParams);

        $requestHandlerProphecy = $this->prophesize(RequestHandlerInterface::class);

        $subject->process($requestProphecy->reveal(), $requestHandlerProphecy->reveal());
    }

    /**
     * @param string $httpHost HTTP_HOST string
     * @param string $hostNamePattern trusted hosts pattern (not used in this test currently)
     * @test
     * @dataProvider hostnamesNotMatchingTrustedHostsConfigurationDataProvider
     */
    public function processAllowsAllHostnameValuesIfHostPatternIsSetToAllowAll(string $httpHost, string $hostNamePattern): void
    {
        $serverParams = $_SERVER;
        $serverParams['HTTP_HOST'] = $httpHost;

        $subject = new VerifyHostHeader(VerifyHostHeader::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL);
        $requestProphecy = $this->prophesize(ServerRequestInterface::class);
        $requestProphecy->getServerParams()->willReturn($serverParams);

        $responseProphecy = $this->prophesize(ResponseInterface::class);

        $requestHandlerProphecy = $this->prophesize(RequestHandlerInterface::class);
        $requestHandlerProphecy->handle($requestProphecy)->willReturn($responseProphecy->reveal())->shouldBeCalled();

        $subject->process($requestProphecy->reveal(), $requestHandlerProphecy->reveal());
    }
}
        'typo3/cms-core/verify-host-header' => [
            'target' => \TYPO3\CMS\Core\Middleware\VerifyHostHeader::class,
            'after' => [
                'typo3/cms-frontend/timetracker',
            ],
        ],
        /** internal: do not use or reference this middleware in your own code */
                'typo3/cms-core/verify-host-header',
use TYPO3\CMS\Core\Middleware\VerifyHostHeader;
    /**
     * @var VerifyHostHeader
     */
    private $verifyHostHeader;

        VerifyHostHeader $verifyHostHeader,
        $this->verifyHostHeader = $verifyHostHeader;
     * @param ServerRequestInterface $request
    public function checkTrustedHostsPatternAction(ServerRequestInterface $request): ResponseInterface
        $serverParams = $request->getServerParams();
        $host = $serverParams['HTTP_HOST'] ?? '';

            'success' => $this->verifyHostHeader->isAllowedHostHeaderValue($host, $serverParams),
     * @param ServerRequestInterface $request
    public function executeAdjustTrustedHostsPatternAction(ServerRequestInterface $request): ResponseInterface
        $serverParams = $request->getServerParams();
        $host = $serverParams['HTTP_HOST'] ?? '';

        if (!$this->verifyHostHeader->isAllowedHostHeaderValue($host, $serverParams)) {
use TYPO3\CMS\Core\Middleware\VerifyHostHeader;
            $container->get(VerifyHostHeader::class),
use TYPO3\CMS\Core\Middleware\VerifyHostHeader;
        if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] === VerifyHostHeader::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL) {
            $verifyHostHeader = new VerifyHostHeader($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] ?? '');
            if ($verifyHostHeader->isAllowedHostHeaderValue($_SERVER['HTTP_HOST'], $_SERVER)) {
    'TYPO3\CMS\Core\Utility\GeneralUtility::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL' => [
        'restFiles' => [
            'Deprecation-95395-GeneralUtilityIsAllowedHostHeaderValueAndTrustedHostsPatternConstants.rst',
        ],
    ],
    'TYPO3\CMS\Core\Utility\GeneralUtility::ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME' => [
        'restFiles' => [
            'Deprecation-95395-GeneralUtilityIsAllowedHostHeaderValueAndTrustedHostsPatternConstants.rst',
        ],
    ],
    'TYPO3\CMS\Core\Utility\GeneralUtility::isAllowedHostHeaderValue' => [
        'numberOfMandatoryArguments' => 1,
        'maximumNumberOfArguments' => 1,
        'restFiles' => [
            'Deprecation-95395-GeneralUtilityIsAllowedHostHeaderValueAndTrustedHostsPatternConstants.rst',
        ],
    ],
use TYPO3\CMS\Core\Middleware\VerifyHostHeader;
        if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] === VerifyHostHeader::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL) {