diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index 6971718..19e9817 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -201,6 +201,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. @@ -217,6 +220,15 @@ 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 + $hostPatterns = Settings::get('trusted_host_patterns', array()); + if (PHP_SAPI !== 'cli' && !empty($hostPatterns)) { + if (static::setupTrustedHosts($request, $hostPatterns) === 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. @@ -1254,4 +1266,46 @@ 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. + * + * 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 + * @param array $hostPatterns + * The array of trusted host patterns + * + * @return boolean + * TRUE if the Host header is trusted, FALSE otherwise + * + * @see https://www.drupal.org/node/1992030 + */ + protected static function setupTrustedHosts(Request $request, $hostPatterns) { + $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/system/src/PathBasedBreadcrumbBuilder.php b/core/modules/system/src/PathBasedBreadcrumbBuilder.php index 0569331..aa839f3 100644 --- a/core/modules/system/src/PathBasedBreadcrumbBuilder.php +++ b/core/modules/system/src/PathBasedBreadcrumbBuilder.php @@ -14,6 +14,7 @@ use Drupal\Core\Link; use Drupal\Core\ParamConverter\ParamNotConvertedException; use Drupal\Core\PathProcessor\InboundPathProcessorInterface; +use Drupal\Core\Routing\RequestContext; use Drupal\Core\Routing\RouteMatch; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; @@ -24,7 +25,6 @@ use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\Routing\Matcher\RequestMatcherInterface; -use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\Exception\MethodNotAllowedException; use Symfony\Component\Routing\Exception\ResourceNotFoundException; @@ -37,7 +37,7 @@ class PathBasedBreadcrumbBuilder implements BreadcrumbBuilderInterface { /** * The router request context. * - * @var \Symfony\Component\Routing\RequestContext + * @var \Drupal\Core\Routing\RequestContext */ protected $context; @@ -86,7 +86,7 @@ class PathBasedBreadcrumbBuilder implements BreadcrumbBuilderInterface { /** * Constructs the PathBasedBreadcrumbBuilder. * - * @param \Symfony\Component\Routing\RequestContext $context + * @param \Drupal\Core\Routing\RequestContext $context * The router request context. * @param \Drupal\Core\Access\AccessManagerInterface $access_manager * The menu link access service. @@ -182,7 +182,7 @@ protected function getRequestForPath($path, array $exclude) { } // @todo Use the RequestHelper once https://drupal.org/node/2090293 is // fixed. - $request = Request::create($this->context->getBaseUrl() . '/' . $path); + $request = Request::create( $this->context->getCompleteBaseUrl() . '/' . $path); // Performance optimization: set a short accept header to reduce overhead in // AcceptHeaderMatcher when matching the request. $request->headers->set('Accept', 'text/html'); diff --git a/core/modules/system/src/Tests/System/TrustedHostsTest.php b/core/modules/system/src/Tests/System/TrustedHostsTest.php new file mode 100644 index 0000000..aac720f --- /dev/null +++ b/core/modules/system/src/Tests/System/TrustedHostsTest.php @@ -0,0 +1,62 @@ +drupalCreateUser(array( + 'administer site configuration', + )); + $this->drupalLogin($admin_user); + } + + /** + * Tests that the status page shows a warning when the trusted host setting + * is missing from settings.php + */ + public function testStatusPageWithoutConfiguration() { + $this->drupalGet('admin/reports/status'); + + $this->assertRaw(t('Trusted Host Settings')); + $this->assertRaw(t('The trusted_host_patterns setting is not configured in settings.php.')); + } + + /** + * Tests that the status page shows a warning when the trusted host setting + * is missing from settings.php + */ + public function testStatusPageWithConfiguration() { + $settings['settings']['trusted_host_patterns'] = (object) array( + 'value' => array('^' . preg_quote(\Drupal::request()->getHost()) . '$'), + 'required' => TRUE, + ); + + $this->writeSettings($settings); + + $this->drupalGet('admin/reports/status'); + $this->assertResponse(200, 'The status page is reachable.'); + + $this->assertRaw(t('Trusted Host Settings')); + $this->assertNoRaw(t('The trusted_host_patterns setting is not configured in settings.php.')); + } + +} diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 08e7865..98f5dc0 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -611,6 +611,27 @@ function system_requirements($phase) { ); } } + + // See if trusted hostnames have been configured, and warn the user if not + // set. + if ($phase == 'runtime') { + $trusted_host_patterns = Settings::get('trusted_host_patterns'); + if (empty($trusted_host_patterns)) { + $requirements['trusted_host_patterns'] = array( + 'title' => t('Trusted Host Settings'), + 'value' => t('Not enabled'), + 'description' => t('The trusted_host_patterns setting is not configured in settings.php. This can lead to security vulnerabilities. It is highly recommended that you configure this. See Protecting against HTTP HOST Header attacks for more information.', array('@url' => 'https://www.drupal.org/node/1992030')), + 'severity' => REQUIREMENT_WARNING, + ); + } + else { + $requirements['trusted_host_patterns'] = array( + 'title' => t('Trusted Host Settings'), + 'value' => t('Enabled'), + ); + } + } + return $requirements; } diff --git a/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php b/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php index 7daf8c2..fb53029 100644 --- a/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php +++ b/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php @@ -58,7 +58,7 @@ class PathBasedBreadcrumbBuilderTest extends UnitTestCase { /** * The mocked route request context. * - * @var \Symfony\Component\Routing\RequestContext|\PHPUnit_Framework_MockObject_MockObject + * @var \Drupal\Core\Routing\RequestContext|\PHPUnit_Framework_MockObject_MockObject */ protected $context; @@ -89,7 +89,7 @@ protected function setUp() { $config_factory = $this->getConfigFactoryStub(array('system.site' => array('front' => 'test_frontpage'))); $this->pathProcessor = $this->getMock('\Drupal\Core\PathProcessor\InboundPathProcessorInterface'); - $this->context = $this->getMock('\Symfony\Component\Routing\RequestContext'); + $this->context = $this->getMock('\Drupal\Core\Routing\RequestContext'); $this->accessManager = $this->getMock('\Drupal\Core\Access\AccessManagerInterface'); $this->titleResolver = $this->getMock('\Drupal\Core\Controller\TitleResolverInterface'); diff --git a/core/modules/views_ui/src/ViewUI.php b/core/modules/views_ui/src/ViewUI.php index e2819d6..c2a4386 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..150a724 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/DrupalKernel/DrupalKernelTrustedHostsTest.php @@ -0,0 +1,78 @@ + array( + '^example\.com$', + '^.+\.example\.com$', + '^example\.org', + '^.+\.example\.org', + ) + )); + + if (!empty($host)) { + $request->headers->set('HOST', $host); + } + + $request->server->set('SERVER_NAME', $server_name); + + $method = new \ReflectionMethod('Drupal\Core\DrupalKernel', 'setupTrustedHosts'); + $method->setAccessible(TRUE); + $valid_host = $method->invoke(null, $request, $settings->get('trusted_host_patterns', array())); + + $this->assertSame($expected, $valid_host, $message); + } + + /** + * Provides test data for testTrustedHosts(). + */ + public function providerTestTrustedHosts() { + $data = []; + + // 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 76d26fc..7acad5a 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -607,3 +607,40 @@ # if (file_exists(__DIR__ . '/settings.local.php')) { # include __DIR__ . '/settings.local.php'; # } + +/** + * Trusted host configuration. + * + * Drupal core can use the Symfony trusted host mechanism to prevent HTTP Host + * header spoofing. + * + * To enable the trusted host mechanism, you enable your allowable hosts + * in $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( + * '^www\.example\.com$', + * ); + * @endcode + * will allow the site to only run from www.example.com. + * + * 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 should specify all of the host patterns that are + * allowed by your site. + * + * 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 variants of example.com and + * example.org, with all subdomains included. + */