diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index d5e6059..c753495 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -206,6 +206,9 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface { * from disk. Defaults to TRUE. * * @return static + * + * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * In case the host name in the request is not trusted. */ public static function createFromRequest(Request $request, $class_loader, $environment, $allow_dumping = TRUE) { // Include our bootstrap file. @@ -222,6 +225,16 @@ public static function createFromRequest(Request $request, $class_loader, $envir $kernel->setSitePath($site_path); Settings::initialize(dirname($core_root), $site_path, $class_loader); + // Initialize our list of trusted HTTP Host headers to protect against + // header attacks. This can be bypassed by setting + // $settings['bypass_trusted_hosts'] = TRUE; + $bypass_trusted_hosts = Settings::get('bypass_trusted_hosts', FALSE); + if (PHP_SAPI !== 'cli' && !$bypass_trusted_hosts) { + if (static::setupTrustedHosts($request) === FALSE) { + throw new BadRequestHttpException(); + } + } + // Redirect the user to the installation script if Drupal has not been // installed yet (i.e., if no $databases array has been defined in the // settings.php file) and we are not already installing. @@ -1313,4 +1326,66 @@ public static function validateHostname(Request $request) { return TRUE; } + /** + * Sets up the lists of trusted HTTP Host headers. + * + * Since the HTTP Host header can be set by the user making the request, it + * is possible to create an attack vectors against a site by overriding this. + * Symfony provides a mechanism for creating a list of trusted Host values. + * + * The default list of trusted hosts is set to + * - localhost + * - locahost.* + * - *.local + * - the value of $_SERVER['SERVER_NAME'], which is set by the system + * administrator. + * + * The default list should be sufficient for installations running a single + * site off of a canonical domain name. Additional host patterns (as + * regular expressions) can be configured throught settings.php for multisite + * installations, sites using ServerAlias without canonical redirection, or + * configurations where the site responds to default requests. For example, + * + * @code + * $settings['trusted_host_patterns'] = array( + * '^example\.com$', + * '^*.example\.com$', + * ); + * @endcode + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object + * + * @return boolean + * TRUE if the Host header is trusted, FALSE otherwise + * + * @see https://www.drupal.org/node/1992030 + */ + public static function setupTrustedHosts(Request $request) { + $hostPatterns = Settings::get('trusted_host_patterns', array()); + + // Allow an empty Host header + $hostPatterns = array_merge($hostPatterns, array( + '^localhost$', + '^localhost\.*', + '\.local$', + )); + + $server_name = $request->server->get('SERVER_NAME'); + if (!empty($server_name)) { + $hostPatterns[] = '^' . str_replace('.', '\.', $server_name . '$'); + } + + $request->setTrustedHosts($hostPatterns); + + // Get the host, which will validate the current request. + try { + $request->getHost(); + } + catch (\UnexpectedValueException $e) { + return FALSE; + } + + return TRUE; + } } diff --git a/core/modules/views_ui/src/ViewUI.php b/core/modules/views_ui/src/ViewUI.php index 58a1734..b8c290f 100644 --- a/core/modules/views_ui/src/ViewUI.php +++ b/core/modules/views_ui/src/ViewUI.php @@ -611,8 +611,9 @@ public function renderPreview($display_id, $args = array()) { // Make view links come back to preview. - // Also override the current path so we get the pager. - $request = new Request(); + // Also override the current path so we get the pager, and make sure the + // Request object gets all of proper values from $_SERVER. + $request = Request::createFromGlobals(); $request->attributes->set(RouteObjectInterface::ROUTE_NAME, 'entity.view.preview_form'); $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, \Drupal::service('router.route_provider')->getRouteByName('entity.view.preview_form')); $request->attributes->set('view', $this->storage); diff --git a/core/tests/Drupal/Tests/Core/DrupalKernel/DrupalKernelTrustedHostsTest.php b/core/tests/Drupal/Tests/Core/DrupalKernel/DrupalKernelTrustedHostsTest.php new file mode 100644 index 0000000..828901f --- /dev/null +++ b/core/tests/Drupal/Tests/Core/DrupalKernel/DrupalKernelTrustedHostsTest.php @@ -0,0 +1,129 @@ +headers->set('HOST', $host); + $request->server->set('SERVER_NAME', $server_name); + + $valid_host = DrupalKernel::setupTrustedHosts($request); + + $this->assertSame($expected, $valid_host, $message); + } + + /** + * Provides test data for testTrustedHosts(). + */ + public function providerTestTrustedHosts() { + $data = []; + + // Test our hardcoded defaults for local development with non-production + // server configurations. + $data[] = ['localhost', '', 'localhost is trusted', TRUE]; + $data[] = ['localhost.d8', '', 'localhost.d8 is trusted', TRUE]; + $data[] = ['d8.local', '', 'd8.local is trusted', TRUE]; + + // Tests canonical URL + $data[] = ['www.example.com', 'www.example.com', 'canonical URL is trusted', TRUE]; + + // Tests missing hostname for HTTP/1.0 compatability where the Host + // header is optional + $data[] = [NULL, 'www.example.com', 'empty Host is valid', TRUE]; + + // Tests mismatches + $data[] = ['example.com', 'www.example.com', 'non-canonical host is not trusted', FALSE]; + $data[] = ['subdomain.example.com', 'www.example.com', 'host with subdomain is not trusted', FALSE]; + $data[] = ['www.example.org', 'www.example.com', 'host with different TLD is not trusted', FALSE]; + $data[] = ['example.org', 'www.example.com', 'host with different TLD is not trusted', FALSE]; + $data[] = ['www.blackhat.com', 'www.example.com', 'unspecified host is untrusted', FALSE]; + + return $data; + } + + /** + * Tests hostname validation with settings. + * + * @covers ::setupTrustedHosts() + * + * @dataProvider providerTestTrustedHostsWithSettings + */ + public function testTrustedHostsWithSettings($host, $server_name, $message, $expected = FALSE) { + $settings = new Settings(array( + 'trusted_host_patterns' => array( + '^example\.com$', + '^.+\.example\.com$', + '^example\.org', + '^.+\.example\.org', + ) + )); + + $request = new Request(); + + if (!empty($host)) { + $request->headers->set('HOST', $host); + } + + $request->server->set('SERVER_NAME', $server_name); + + $valid_host = DrupalKernel::setupTrustedHosts($request); + + $this->assertSame($expected, $valid_host, $message); + } + + /** + * Provides test data for testTrustedHostsWithSettings(). + */ + public function providerTestTrustedHostsWithSettings() { + $data = []; + + // Test our hardcoded defaults for local development with non-production + // server configurations. + $data[] = ['localhost', '', 'localhost is trusted', TRUE]; + $data[] = ['localhost.d8', '', 'localhost.d8 is trusted', TRUE]; + $data[] = ['d8.local', '', 'd8.local is trusted', TRUE]; + + // Tests canonical URL + $data[] = ['www.example.com', 'www.example.com', 'canonical URL is trusted', TRUE]; + + // Tests missing hostname for HTTP/1.0 compatability where the Host + // header is optional + $data[] = [NULL, 'www.example.com', 'empty Host is valid', TRUE]; + + // Tests the additional paterns from the settings. + $data[] = ['example.com', 'www.example.com', 'host from settings is trusted', TRUE]; + $data[] = ['subdomain.example.com', 'www.example.com', 'host from settings is trusted', TRUE]; + $data[] = ['www.example.org', 'www.example.com', 'host from settings is trusted', TRUE]; + $data[] = ['example.org', 'www.example.com', 'host from settings is trusted', TRUE]; + + // Tests mismatches + $data[] = ['www.blackhat.com', 'www.example.com', 'unspecified host is untrusted', FALSE]; + + return $data; + } + +} diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index e21206b..8e3a951 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -631,3 +631,36 @@ # if (file_exists(__DIR__ . '/settings.local.php')) { # include __DIR__ . '/settings.local.php'; # } + +/** + * Trusted host configuration. + * + * Drupal core uses the Symfony trusted host mechanism to prevent HTTP Host + * header spoofing. To full enable this, your webserver must be configured to + * report the canonical name in $_SERVER['SERVER_NAME']. + * + * If you are running multisite, or if you are running your site from + * different domain names (eg, you don't redirect http://www.example.com to + * http://example.com), you can configure additional trusted host patterns + * with $settings['trusted_host_patterns']. This should be an array of + * regular expression paterns, without delimiters, representing the hosts you + * would like to allow. + * + * For example: + * @code + * $settings['trusted_host_patterns'] = array( + * '^example\.com$', + * '^.+\.example\.com$', + * '^example\.org', + * '^.+\.example\.org', + * ); + * @endcode + * will allow the site to run off of all variantes of example.com and + * example.org, with all subdomains included. + * + * @see http://httpd.apache.org/docs/current/mod/core.html#usecanonicalname + * @see http://stackoverflow.com/questions/2297403/http-host-vs-server-name/2297421#2297421 + * + * Remove the leading hash sign if you would like to disable this functionality. + */ +# $settings['bypass_trusted_hosts'] = TRUE;