[SECURITY] Add trusted HTTP_HOST configuration 75/30275/2
authorHelmut Hummel <helmut.hummel@typo3.org>
Thu, 22 May 2014 07:31:31 +0000 (09:31 +0200)
committerOliver Hader <oliver.hader@typo3.org>
Thu, 22 May 2014 07:31:35 +0000 (09:31 +0200)
TYPO3 uses the values of HTTP_HOST in several
places without validating them. This could
lead to a situation where links are generated
using the host part from HTTP_HOST.
Since HTTP_HOST headers are user input and
can be spoofed by an attacker, it leads
into several potential and actual security issues.
To address this, a configuration option for
trusted hosts is added, which is evaluated every
time getIndpEnv('HTTP_HOST') is called.
The configuration option is
$GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern']
and can contain either a regular expression or the
value "SERVER_NAME"
To properly output the exception message in case
the trustedHostPattern does not match,
we need to adapt the exception handlers slightly
to not log information in this case and to actually
show the message even in production context to not
confuse admins on what is currently going wrong.
To not break all existing installations, the default
pattern is set to 'SERVER_NAME' which allows all
HTTP_HOST values matching the SERVER_NAME (and
optionally the SERVER_PORT if a port is specified
in the HTTP_HOST value).
This will secure all installation which use properly
configured name based virtual hosts, but leaves
installations where the web server is not bound
to a specific host name still in an insecure state.
Fixes: #30377
Releases: 6.2, 6.1, 6.0, 4.7, 4.5
Security-Bulletin: TYPO3-CORE-SA-2014-001

Change-Id: Id210212e6fbd186a273f92b340d5060e9c6f900d
Reviewed-on: https://review.typo3.org/30275
Reviewed-by: Oliver Hader
Tested-by: Oliver Hader
t3lib/class.t3lib_div.php
t3lib/config_default.php
t3lib/error/class.t3lib_error_abstractexceptionhandler.php
t3lib/message/class.t3lib_message_errorpagemessage.php

index b31ebe0..78ac053 100644 (file)
@@ -236,6 +236,17 @@ final class t3lib_div {
        const SYSLOG_SEVERITY_ERROR = 3;
        const SYSLOG_SEVERITY_FATAL = 4;
 
+       const ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL = '.*';
+       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
+        */
+       static protected $allowHostHeaderValue = FALSE;
+
        /**
         * Singleton instances returned by makeInstance, using the class names as
         * array keys
@@ -4072,6 +4083,12 @@ final class t3lib_div {
                                                $retVal = $host;
                                        }
                                }
+                               if (!self::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;
                                // These are let through without modification
                        case 'HTTP_REFERER':
@@ -4193,6 +4210,51 @@ final class t3lib_div {
        }
 
        /**
+        * 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.
+        *
+        * @param string $hostHeaderValue HTTP_HOST header value as sent during the request (may include port)
+        * @return bool
+        */
+       static public function isAllowedHostHeaderValue($hostHeaderValue) {
+               if (self::$allowHostHeaderValue === TRUE) {
+                       return TRUE;
+               }
+
+               // Allow all install tool requests
+               // 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
+               if (defined('TYPO3_REQUESTTYPE') && (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_INSTALL) || (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_CLI)) {
+                       return self::$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) {
+                       self::$allowHostHeaderValue = TRUE;
+               } elseif ($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] === self::ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME) {
+                       // Allow values that equal the server name
+                       // Note that this is only secure if name base virtual host are configured correctly in the webserver
+                       $defaultPort = self::getIndpEnv('TYPO3_SSL') ? '443' : '80';
+                       $parsedHostValue = parse_url('http://' . $hostHeaderValue);
+                       if (isset($parsedHostValue['port'])) {
+                               self::$allowHostHeaderValue = ($parsedHostValue['host'] === $_SERVER['SERVER_NAME'] && (string)$parsedHostValue['port'] === $_SERVER['SERVER_PORT']);
+                       } else {
+                               self::$allowHostHeaderValue = ($hostHeaderValue === $_SERVER['SERVER_NAME'] && $defaultPort === $_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
+                       self::$allowHostHeaderValue = (bool)preg_match('/^' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] . '$/', $hostHeaderValue);
+               }
+
+               return self::$allowHostHeaderValue;
+       }
+
+       /**
         * Gets the unixtime as milliseconds.
         *
         * @return      integer         The unixtime as milliseconds
index 08a3913..5039442 100644 (file)
@@ -68,6 +68,7 @@ $TYPO3_CONF_VARS = array(
                'cookieHttpOnly' => FALSE,                              // Boolean: When enabled the cookie will be made accessible only through the HTTP protocol. This means that the cookie won't be accessible by scripting languages, such as JavaScript. This setting can effectively help to reduce identity theft through XSS attacks (although it is not supported by all browsers).
                'doNotCheckReferer' => FALSE,                   // Boolean: If set, it's NOT checked numerous places that the refering host is the same as the current. This is an option you should set if you have problems with proxies not passing the HTTP_REFERER variable.
                'recursiveDomainSearch' => FALSE,               // Boolean: If set, the search for domain records will be done recursively by stripping parts of the host name off until a matching domain record is found.
+               'trustedHostsPattern' => 'SERVER_NAME', // String: Regular expression pattern that matches all allowed hostnames (including their ports) of this TYPO3 installation, or the string "SERVER_NAME" (default). The default value <code>SERVER_NAME</code> checks if the HTTP Host header equals the SERVER_NAME and SERVER_PORT. This is secure in correctly configured hosting environments and does not need further configuration. If you cannot change your hosting environment, you can enter a regular expression here. Examples: <code>.*\.domain\.com</code> matches all hosts that end with <code>.domain.com</code> with all corresponding subdomains. <code>(.*\.domain|.*\.otherdomain)\.com</code> matches all hostnames with subdomains from <code>.domain.com</code> and <code>.otherdomain.com</code>. Be aware that HTTP Host header may also contain a port. If your installation runs on a specific port, you need to explicitly allow this in your pattern, e.g. <code>www\.domain\.com:88</code> allows only <code>www.domain.com:88</code>, <strong>not</strong> <code>www.domain.com</code>. To disable this check completely (not recommended because it is <strong>insecure</strong>) you can use ".*" as pattern.
                'devIPmask' => '127.0.0.1,::1',                 // Defines a list of IP addresses which will allow development-output to display. The debug() function will use this as a filter. See the function t3lib_div::cmpIP() for details on syntax. Setting this to blank value will deny all. Setting to "*" will allow all.
                'sqlDebug' => 0,                                                // <p>Integer (0, 1, 2). Allows displaying executed SQL queries in the browser (for debugging purposes and development)</p><dl><dt>0</dt><dd>no SQL shown (default)</dd><dt>1</dt><dd>show only failed queries</dd><dt>2</dt><dd>show all queries</dd></dl>
                'enable_DLOG' => FALSE,                                 // Boolean: Whether the developer log is enabled. See constant "TYPO3_DLOG"
index a487903..b54dab9 100644 (file)
@@ -62,6 +62,10 @@ abstract class t3lib_error_AbstractExceptionHandler implements t3lib_error_Excep
         * @see t3lib_div::sysLog(), t3lib_div::devLog()
         */
        protected function writeLogEntries(Exception $exception, $context) {
+                       // Do not write any logs for this message to avoid filling up tables or files with illegal requests
+               if ($exception->getCode() === 1396795884) {
+                       return;
+               }
                $filePathAndName = $exception->getFile();
                $exceptionCodeNumber = ($exception->getCode() > 0) ? '#' . $exception->getCode() . ': ' : '';
                $logTitle = 'Core: Exception handler (' . $context . ')';
index 57728d8..f2c78e8 100644 (file)
@@ -97,7 +97,10 @@ class t3lib_message_ErrorpageMessage extends t3lib_message_AbstractMessage {
                        '###CSS_CLASS###'     => $classes[$this->severity],
                        '###TITLE###'         => $this->title,
                        '###MESSAGE###'       => $this->message,
-                       '###BASEURL###'       => t3lib_div::getIndpEnv('TYPO3_SITE_URL'),
+                       // Avoid calling TYPO3_SITE_URL here to get the base URL as it might be that we output an exception message with
+                       // invalid trusted host, which would lead to a nested exception! See: #30377
+                       // Instead we calculate the relative path to the document root without involving HTTP request parameters.
+                       '###BASEURL###' => substr(PATH_site, strlen(t3lib_div::getIndpEnv('TYPO3_DOCUMENT_ROOT'))),
                        '###TYPO3_mainDir###' => TYPO3_mainDir,
                        '###TYPO3_copyright_year###' => TYPO3_copyright_year,
                );