diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index ee45da3..87b28e5 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. @@ -1314,4 +1327,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..4ab4215 100644 --- a/core/modules/views_ui/src/ViewUI.php +++ b/core/modules/views_ui/src/ViewUI.php @@ -612,7 +612,7 @@ 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(); + $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); @@ -622,6 +622,12 @@ public function renderPreview($display_id, $args = array()) { $raw_parameters->set('display_id', $display_id); $request->attributes->set('_raw_variables', $raw_parameters); + $hostPatterns = $current_request->getTrustedHosts(); + $hostPatterns = array_map(function ($hostPattern) { + return substr($hostPattern, 1, -2); + }, $hostPatterns); + $request->setTrustedHosts($hostPatterns); + foreach ($args as $key => $arg) { $request->attributes->set('arg_' . $key, $arg); } diff --git a/core/tests/Drupal/Tests/Core/DrupalKernel/TrustedHostsTest.php b/core/tests/Drupal/Tests/Core/DrupalKernel/TrustedHostsTest.php new file mode 100644 index 0000000..c1082f8 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/DrupalKernel/TrustedHostsTest.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; + } + +}