From 62ee12bbff072ffb211d4dea670f2765c7c926cb Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Fri, 21 Nov 2014 09:31:37 +0000
Subject: [PATCH] Issue #2304949 by mpdonadio, cilefen, znerol, klausi,
 gaurav.goyal, regilero: Port HTTP Host header DoS fix from SA-CORE-2014-003

---
 core/authorize.php                            | 14 +++-
 core/lib/Drupal/Core/DrupalKernel.php         | 69 ++++++++++++++++---
 core/rebuild.php                              | 12 +++-
 .../DrupalKernel/ValidateHostnameTest.php     | 67 ++++++++++++++++++
 index.php                                     |  6 ++
 5 files changed, 155 insertions(+), 13 deletions(-)
 create mode 100644 core/tests/Drupal/Tests/Core/DrupalKernel/ValidateHostnameTest.php

diff --git a/core/authorize.php b/core/authorize.php
index 231ad8d629d1..fe4b7d0c4fef 100644
--- a/core/authorize.php
+++ b/core/authorize.php
@@ -22,6 +22,7 @@
 
 use Drupal\Core\DrupalKernel;
 use Drupal\Core\Url;
+use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Drupal\Core\Site\Settings;
@@ -54,9 +55,16 @@ function authorize_access_allowed() {
   return Settings::get('allow_authorize_operations', TRUE) && \Drupal::currentUser()->hasPermission('administer software updates');
 }
 
-$request = Request::createFromGlobals();
-$kernel = DrupalKernel::createFromRequest($request, $autoloader, 'prod');
-$kernel->prepareLegacyRequest($request);
+try {
+  $request = Request::createFromGlobals();
+  $kernel = DrupalKernel::createFromRequest($request, $autoloader, 'prod');
+  $kernel->prepareLegacyRequest($request);
+}
+catch (HttpExceptionInterface $e) {
+  $response = new Response('', $e->getStatusCode());
+  $response->prepare($request)->send();
+  exit;
+}
 
 // We have to enable the user and system modules, even to check access and
 // display errors via the maintenance theme.
diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php
index 9c926ea3357d..dc40e4fd81f8 100644
--- a/core/lib/Drupal/Core/DrupalKernel.php
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -29,6 +29,7 @@
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Symfony\Component\HttpKernel\TerminableInterface;
 use Composer\Autoload\ClassLoader;
 
@@ -292,12 +293,19 @@ public function __construct($environment, $class_loader, $allow_dumping = TRUE)
    * @return string
    *   The path of the matching directory.
    *
+   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   *   In case the host name in the request is invalid.
+   *
    * @see \Drupal\Core\DrupalKernelInterface::getSitePath()
    * @see \Drupal\Core\DrupalKernelInterface::setSitePath()
    * @see default.settings.php
    * @see example.sites.php
    */
   public static function findSitePath(Request $request, $require_settings = TRUE) {
+    if (static::validateHostname($request) === FALSE) {
+      throw new BadRequestHttpException();
+    }
+
     // Check for a simpletest override.
     if ($test_prefix = drupal_valid_test_ua()) {
       return 'sites/simpletest/' . substr($test_prefix, 10);
@@ -313,7 +321,7 @@ public static function findSitePath(Request $request, $require_settings = TRUE)
     if (!$script_name) {
       $script_name = $request->server->get('SCRIPT_FILENAME');
     }
-    $http_host = $request->server->get('HTTP_HOST');
+    $http_host = $request->getHost();
 
     $sites = array();
     include DRUPAL_ROOT . '/sites/sites.php';
@@ -815,8 +823,7 @@ protected function initializeRequestGlobals(Request $request) {
     }
     else {
       // Create base URL.
-      $http_protocol = $request->isSecure() ? 'https' : 'http';
-      $base_root = $http_protocol . '://' . $request->server->get('HTTP_HOST');
+      $base_root = $request->getSchemeAndHttpHost();
 
       $base_url = $base_root;
 
@@ -898,16 +905,13 @@ protected function initializeCookieGlobals(Request $request) {
       // Replace "core" out of session_name so core scripts redirect properly,
       // specifically install.php.
       $session_name = preg_replace('/\/core$/', '', $session_name);
-      // HTTP_HOST can be modified by a visitor, but has been sanitized already
-      // in DrupalKernel::bootEnvironment().
-      if ($cookie_domain = $request->server->get('HTTP_HOST')) {
-        // Strip leading periods, www., and port numbers from cookie domain.
+      if ($cookie_domain = $request->getHost()) {
+        // Strip leading periods and www. from cookie domain.
         $cookie_domain = ltrim($cookie_domain, '.');
         if (strpos($cookie_domain, 'www.') === 0) {
           $cookie_domain = substr($cookie_domain, 4);
         }
-        $cookie_domain = explode(':', $cookie_domain);
-        $cookie_domain = '.' . $cookie_domain[0];
+        $cookie_domain = '.' . $cookie_domain;
       }
     }
     // Per RFC 2109, cookie domains must contain at least one dot other than the
@@ -1255,4 +1259,51 @@ protected function classLoaderAddMultiplePsr4(array $namespaces = array()) {
     }
   }
 
+  /**
+   * Validates a hostname length.
+   *
+   * @param string $host
+   *   A hostname.
+   *
+   * @return bool
+   *   TRUE if the length is appropriate, or FALSE otherwise.
+   */
+  protected static function validateHostnameLength($host) {
+    // Limit the length of the host name to 1000 bytes to prevent DoS attacks
+    // with long host names.
+    return strlen($host) <= 1000
+    // Limit the number of subdomains and port separators to prevent DoS attacks
+    // in findSitePath().
+    && substr_count($host, '.') <= 100
+    && substr_count($host, ':') <= 100;
+  }
+
+  /**
+   * Validates the hostname supplied from the HTTP request.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object
+   *
+   * @return bool
+   *   TRUE if the hostmame is valid, or FALSE otherwise.
+   *
+   * @todo Adjust per resolution to https://github.com/symfony/symfony/issues/12349
+   */
+  public static function validateHostname(Request $request) {
+    // $request->getHost() can throw an UnexpectedValueException if it
+    // detects a bad hostname, but it does not validate the length.
+    try {
+      $http_host = $request->getHost();
+    }
+    catch (\UnexpectedValueException $e) {
+      return FALSE;
+    }
+
+    if (static::validateHostnameLength($http_host) === FALSE) {
+      return FALSE;
+    }
+
+    return TRUE;
+  }
+
 }
diff --git a/core/rebuild.php b/core/rebuild.php
index be3e603b3548..7ca8af73bf2f 100644
--- a/core/rebuild.php
+++ b/core/rebuild.php
@@ -13,7 +13,9 @@
 use Drupal\Component\Utility\Crypt;
 use Drupal\Core\DrupalKernel;
 use Drupal\Core\Site\Settings;
+use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
 
 // Change the directory to the Drupal root.
 chdir('..');
@@ -25,7 +27,15 @@
 // Manually resemble early bootstrap of DrupalKernel::boot().
 require_once __DIR__ . '/includes/bootstrap.inc';
 DrupalKernel::bootEnvironment();
-Settings::initialize(dirname(__DIR__), DrupalKernel::findSitePath($request), $autoloader);
+
+try {
+  Settings::initialize(dirname(__DIR__), DrupalKernel::findSitePath($request), $autoloader);
+}
+catch (HttpExceptionInterface $e) {
+  $response = new Response('', $e->getStatusCode());
+  $response->prepare($request)->send();
+  exit;
+}
 
 if (Settings::get('rebuild_access', FALSE) ||
   ($request->get('token') && $request->get('timestamp') &&
diff --git a/core/tests/Drupal/Tests/Core/DrupalKernel/ValidateHostnameTest.php b/core/tests/Drupal/Tests/Core/DrupalKernel/ValidateHostnameTest.php
new file mode 100644
index 000000000000..6d32411cd9a0
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/DrupalKernel/ValidateHostnameTest.php
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\DrupalKernel\ValidateHostnameTest.
+ */
+
+namespace Drupal\Tests\Core\DrupalKernel;
+
+use Drupal\Core\DrupalKernel;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @coversDefaultClass \Drupal\Core\DrupalKernel
+ * @group DrupalKernel
+ */
+class ValidateHostnameTest extends UnitTestCase {
+
+  /**
+   * Tests hostname validation.
+   *
+   * @covers ::validateHostname()
+   *
+   * @dataProvider providerTestValidateHostname
+   */
+  public function testValidateHostname($hostname, $message, $expected = FALSE) {
+    $server = ['HTTP_HOST' => $hostname];
+    $request = new Request([], [], [], [], [], $server);
+    $validated_hostname = DrupalKernel::validateHostname($request);
+    $this->assertSame($expected, $validated_hostname, $message);
+  }
+
+  /**
+   * Provides test data for testValidateHostname().
+   */
+  public function providerTestValidateHostname() {
+    $data = [];
+
+    // Verifies that DrupalKernel::validateHostname() prevents invalid
+    // characters per RFC 952/2181.
+    $data[] = ['security/.drupal.org:80', 'HTTP_HOST with / is invalid'];
+    $data[] = ['security/.drupal.org:80', 'HTTP_HOST with / is invalid'];
+    $data[] = ['security\\.drupal.org:80', 'HTTP_HOST with \\ is invalid'];
+    $data[] = ['security<.drupal.org:80', 'HTTP_HOST with &lt; is invalid'];
+    $data[] = ['security..drupal.org:80', 'HTTP_HOST with .. is invalid'];
+
+    // Verifies hostnames that are too long, or have too many parts are
+    // invalid.
+    $data[] = [str_repeat('x', 1000) . '.security.drupal.org:80', 'HTTP_HOST with more than 1000 characters is invalid.'];
+    $data[] = [str_repeat('x.', 100) . 'security.drupal.org:80', 'HTTP_HOST with more than 100 subdomains is invalid.'];
+    $data[] = ['security.drupal.org:80' . str_repeat(':x', 100), 'HTTP_HOST with more than 100 port separators is invalid.'];
+
+    // Verifies that a valid hostname is allowed.
+    $data[] = ['security.drupal.org:80', 'Properly formed HTTP_HOST is valid.', TRUE];
+
+    // Verifies that using valid IP address for the hostname is allowed.
+    $data[] = ['72.21.91.99:80', 'Properly formed HTTP_HOST with IPv4 address valid.', TRUE];
+    $data[] = ['2607:f8b0:4004:803::1002:80', 'Properly formed HTTP_HOST with IPv6 address valid.', TRUE];
+
+    // Verfies that the IPv6 loopback address is valid.
+    $data[] = ['[::1]:80', 'HTTP_HOST containing IPv6 loopback is valid.', TRUE];
+
+    return $data;
+  }
+
+}
diff --git a/index.php b/index.php
index 406d3dcbe329..55fc9474985a 100644
--- a/index.php
+++ b/index.php
@@ -10,7 +10,9 @@
 
 use Drupal\Core\DrupalKernel;
 use Drupal\Core\Site\Settings;
+use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
 
 $autoloader = require_once __DIR__ . '/core/vendor/autoload.php';
 
@@ -24,6 +26,10 @@
       ->prepare($request)->send();
   $kernel->terminate($request, $response);
 }
+catch (HttpExceptionInterface $e) {
+  $response = new Response('', $e->getStatusCode());
+  $response->prepare($request)->send();
+}
 catch (Exception $e) {
   $message = 'If you have just changed code (for example deployed a new module or moved an existing one) read <a href="http://drupal.org/documentation/rebuild">http://drupal.org/documentation/rebuild</a>';
   if (Settings::get('rebuild_access', FALSE)) {
-- 
GitLab