diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index 0ff2e891479f787479d8bc716b0fbd7391b1776e..d722d6b2db0b34f56ef382f086fc492b8d91a3be 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('The provided host name is not valid for this server.'); + } + } + // 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. @@ -1266,4 +1278,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 05693312e1096626273b82e46f4db48b23e22d2a..6080d6b8f2a8c8cff612821e1f7d7fb7da799744 100644 --- a/core/modules/system/src/PathBasedBreadcrumbBuilder.php +++ b/core/modules/system/src/PathBasedBreadcrumbBuilder.php @@ -7,6 +7,7 @@ namespace Drupal\system; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Access\AccessManagerInterface; use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface; use Drupal\Core\Config\ConfigFactoryInterface; @@ -14,19 +15,17 @@ 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; use Drupal\Core\StringTranslation\StringTranslationTrait; -use Drupal\Component\Utility\Unicode; use Drupal\Core\Url; use Symfony\Component\HttpFoundation\Request; -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; +use Symfony\Component\Routing\Matcher\RequestMatcherInterface; /** * Class to define the menu_link breadcrumb builder. @@ -37,7 +36,7 @@ class PathBasedBreadcrumbBuilder implements BreadcrumbBuilderInterface { /** * The router request context. * - * @var \Symfony\Component\Routing\RequestContext + * @var \Drupal\Core\Routing\RequestContext */ protected $context; @@ -86,7 +85,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 +181,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 0000000000000000000000000000000000000000..2be2c8b70fe58b5844357e732662920e38694bf6 --- /dev/null +++ b/core/modules/system/src/Tests/System/TrustedHostsTest.php @@ -0,0 +1,62 @@ +<?php + +/** + * @file + * Contains \Drupal\system\Tests\System\TrustedHostsTest. + */ + +namespace Drupal\system\Tests\System; + +use Drupal\Core\Site\Settings; +use Drupal\simpletest\WebTestBase; + +/** + * Tests output on the status overview page. + * + * @group system + */ +class TrustedHostsTest extends WebTestBase { + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $admin_user = $this->drupalCreateUser(array( + 'administer site configuration', + )); + $this->drupalLogin($admin_user); + } + + /** + * Tests that the status page shows an error when the trusted host setting + * is missing from settings.php + */ + public function testStatusPageWithoutConfiguration() { + $this->drupalGet('admin/reports/status'); + $this->assertResponse(200, 'The status page is reachable.'); + + $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 the trusted patterns 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->assertRaw(t('The trusted_host_patterns setting is set to allow')); + } + +} diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 08e7865983fdec7b3a09935e659de3445e3616ce..4a8683eaae8ad41b46eaf245a24f50c1f255f560 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -611,6 +611,28 @@ function system_requirements($phase) { ); } } + + // See if trusted hostnames have been configured, and warn the user if they + // are 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 <strong>highly recommended</strong> that you configure this. See <a href="@url">Protecting against HTTP HOST Header attacks</a> for more information.', array('@url' => 'https://www.drupal.org/node/1992030')), + 'severity' => REQUIREMENT_ERROR, + ); + } + else { + $requirements['trusted_host_patterns'] = array( + 'title' => t('Trusted Host Settings'), + 'value' => t('Enabled'), + 'description' => t('The trusted_host_patterns setting is set to allow %trusted_host_patterns', array('%trusted_host_patterns' => join(', ', $trusted_host_patterns))), + ); + } + } + 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 7daf8c2fa9c24bc1d7b3600ea2aaa56067015677..fb530299c40f10d585592f6adab54c29b790933d 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 2f3e417d4252668c241a9c9b0c0333711a4e0902..e8464fa31ecb5b60e361cad57c568ecad5d8eb7c 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 the 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 0000000000000000000000000000000000000000..3a495118e4ec09739ba13fa378a2250f7eed0449 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/DrupalKernel/DrupalKernelTrustedHostsTest.php @@ -0,0 +1,78 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\DrupalKernel\DrupalKernelTrustedHostsTest. + */ + +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 DrupalKernelTrustedHostsTest extends UnitTestCase { + + /** + * Tests hostname validation with settings. + * + * @covers ::setupTrustedHosts() + * + * @dataProvider providerTestTrustedHosts + */ + public function testTrustedHosts($host, $server_name, $message, $expected = FALSE) { + $request = new Request(); + + $trusted_host_patterns = [ + '^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, $trusted_host_patterns); + + $this->assertSame($expected, $valid_host, $message); + + // Reset the trusted hosts because it is statically stored on the request. + $method->invoke(null, $request, []); + } + + /** + * 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 mismatch. + $data[] = ['www.blackhat.com', 'www.example.com', 'unspecified host is untrusted', FALSE]; + + return $data; + } + +} diff --git a/index.php b/index.php index 55fc9474985adafd4c8946221d7fdc903e8862c8..867f0e09215ae4f7a0bc0256e28ea89cc56850c4 100644 --- a/index.php +++ b/index.php @@ -27,7 +27,7 @@ $kernel->terminate($request, $response); } catch (HttpExceptionInterface $e) { - $response = new Response('', $e->getStatusCode()); + $response = new Response($e->getMessage(), $e->getStatusCode()); $response->prepare($request)->send(); } catch (Exception $e) { diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 76d26fcfdf3c5ef11b65b20f518261e885139d1d..7cc10be7260ccade7699c9d7831d6328f6b770d4 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 patterns, 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. + */ diff --git a/sites/example.settings.local.php b/sites/example.settings.local.php index 7859fe5021951e9a393204ae347e5a690f440f9b..3d5857a306b356158793eb72e131e983e5ad3aef 100644 --- a/sites/example.settings.local.php +++ b/sites/example.settings.local.php @@ -55,3 +55,15 @@ * using these parameters in a request to rebuild.php. */ $settings['rebuild_access'] = TRUE; + +/** + * Trust localhost. + * + * This will configure several common hostnames used for local development to + * be trusted hosts. + */ +$settings['trusted_host_patterns'] = array( + '^localhost$', + '^localhost\.*', + '\.local$', +);