Commit 62ee12bb authored by alexpott's avatar alexpott

Issue #2304949 by mpdonadio, cilefen, znerol, klausi, gaurav.goyal, regilero:...

Issue #2304949 by mpdonadio, cilefen, znerol, klausi, gaurav.goyal, regilero: Port HTTP Host header DoS fix from SA-CORE-2014-003
parent b61edd72
......@@ -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.
......
......@@ -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;
}
}
......@@ -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') &&
......
<?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;
}
}
......@@ -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)) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment