Problem/Motivation

Malformed HTTP Host headers can cause DOS vulnerabilities in Drupal because of the way multisite negotiation gets handled. The Host header needs to be validated to prevent this.

Proposed resolution

Now that #2278353: Update to Symfony 2.5 is done, we can refactor DrupalKernel to use Request::getHost() and friends. Request::getHost() does some of what we need. We also need to wrap this to add Drupal-specific from the D7 patch that is missing from Request::getHost().

Remaining tasks

  • #2278353: Update to Symfony 2.5
  • Add a method to DrupalKernel with the additional host name checks.
  • Call the new method before the multisite directories are searched.
  • Throw an exception if the hostname does not validate.
  • Write tests.

User interface changes

None.

API changes

None.

Original report by @klausi

See https://www.drupal.org/SA-CORE-2014-003

The original patch against D7 is attached.

CommentFileSizeAuthor
#106 interdiff-99-106.txt2.29 KBmpdonadio
#106 port_http_host_header-2304949-106.patch10.51 KBmpdonadio
#99 interdiff-96-99.txt3.38 KBmpdonadio
#99 port_http_host_header-2304949-99.patch10.08 KBmpdonadio
#96 interdiff-92-96.txt2.07 KBmpdonadio
#96 port_http_host_header-2304949-96.patch9.93 KBmpdonadio
#95 dos-script-with-patch.txt1.46 KBznerol
#95 dos-script-without-patch.txt1.41 KBznerol
#95 dos.sh_.txt1.09 KBznerol
#92 interdiff-88-92.txt3.58 KBmpdonadio
#92 port_http_host_header-2304949-92.patch9.5 KBmpdonadio
#88 interdiff-83-88.txt7.03 KBmpdonadio
#88 port_http_host_header-2304949-88.patch9.7 KBmpdonadio
#86 2304949-do-not-test.patch1.81 KBmpdonadio
#83 interdiff-77-83.txt5.03 KBmpdonadio
#83 port_http_host_header-2304949-83.patch8.54 KBmpdonadio
#77 interdiff-62-77.txt3.01 KBmpdonadio
#77 port_http_host_header-2304949-77.patch5.61 KBmpdonadio
#74 port_http_host_header-2304949-73.patch7.26 KBgaurav.goyal
#70 interdiff-62-70.txt659 bytesmpdonadio
#70 port_http_host_header-2304949-70.patch7.31 KBmpdonadio
#67 interdiff-62-67.txt5.14 KBmpdonadio
#67 port_http_host_header-2304949-67.patch6.69 KBmpdonadio
#63 interdiff-48-62.txt3.6 KBmpdonadio
#63 interdiff-57-62.txt462 bytesmpdonadio
#63 port_http_host_header-2304949-62.patch2.41 KBmpdonadio
#60 port_http_host_header-2304949-48.patch4.65 KBmpdonadio
#60 interdiff-45-48.txt2.45 KBmpdonadio
#60 test3.sh_.txt186 bytesmpdonadio
#60 test2.sh_.txt200 bytesmpdonadio
#60 test1.sh_.txt201 bytesmpdonadio
#58 interdiff-53-57.txt729 bytescilefen
#58 port_http_host_header-2304949-57.patch2.42 KBcilefen
#53 port_http_host_header-2304949-53.patch2.42 KBcilefen
#52 port_http_host_header-2304949-52.patch2.42 KBcilefen
#48 port_http_host_header-2304949-48.patch2.88 KBcilefen
#45 interdiff-43-45.txt2.09 KBmpdonadio
#45 interdiff-23-45.txt1.79 KBmpdonadio
#45 port_http_host_header-2304949-45.patch2.89 KBmpdonadio
#43 interdiff-23-43.txt786 bytesmpdonadio
#43 port_http_host_header-2304949-43.patch2.5 KBmpdonadio
#25 interdiff.txt583 bytesmpdonadio
#25 port_http_host_header-2304949-25.patch2.31 KBmpdonadio
#24 dos.sh_.txt620 bytesklausi
#23 port_http_host_header-2304949-23.patch2.2 KBcilefen
#23 interdiff-17-23.txt1.32 KBcilefen
#17 port_http_host_header-2304949-17.patch1.6 KBcilefen
#5 port_http_host_header-2304949-5.patch2.07 KBcilefen
sec-D7-conf-path-dos-105258-23.patch1.96 KBklausi

Comments

klausi’s picture

Issue tags: +SA-CORE-2014-003
klausi’s picture

longwave’s picture

drupal_valid_http_host() was removed in #2016629: Refactor bootstrap to better utilize the kernel and we no longer perform any validation, but DrupalKernel::findSitePath() looks very similar to D7's conf_path() so the vulnerability likely still exists.

catch’s picture

Let's add back the validation from 7.x while we're here, looks like an oversight in the other issue.

cilefen’s picture

Status: Active » Needs review
StatusFileSize
new2.07 KB

Just to kick this off, I am trying to figure out where this should go.

cilefen’s picture

Comment removed for security reasons.

larowlan’s picture

https://github.com/sensiolabs/symfony-httpfoundation-request-host/pull/2...
We will need to update our findSitePath to use getHost

larowlan’s picture

cilefen’s picture

Status: Needs review » Postponed

Symfony had the same vulnerability that had to be patched before we could proceed with this issue. Symfony just released a patch. http://symfony.com/blog/cve-2014-5244-denial-of-service-with-a-malicious...

So we must postpone on #2234277: Composer update (includes security fixes) then figure out if anything else needs to be done on this.

cilefen’s picture

catch’s picture

Issue tags: +D8 upgrade path, +beta target

Tagging with D8 upgrade path, see issue summary of #2341575: [meta] Provide a beta to beta/rc upgrade path for why.

cilefen’s picture

Issue summary: View changes
Status: Postponed » Needs work
Related issues: -#2234277: Composer update (includes security fixes) +#2278353: Update to Symfony 2.5

#2278353: Update to Symfony 2.5 is in so this is active.

cilefen’s picture

See Symfony\Component\HttpFoundation\Request::getHost.

cilefen’s picture

Just to be thorough, HEAD is on Symfony 2.5.5 and here is the commit where the fix was added: https://github.com/symfony/symfony/commit/0aaabbfe8b49b436fe321823d89ae8...

cilefen’s picture

cilefen’s picture

Status: Needs work » Needs review
StatusFileSize
new1.6 KB
cilefen’s picture

Issue summary: View changes
larowlan’s picture

Do you think we should bring in the tests too? Although note that we got them into Symfony upstream anyway.

cilefen’s picture

@larowlan I am divided on that. If we were building Drupal from scratch right now and using the Symfony Request class, we would not write a test for this.

klausi’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -314,7 +314,7 @@ public static function findSitePath(Request $request, $require_settings = TRUE)
    +    $http_host = $request->getHost();
    

    So now the method findSitePath() can throw \UnexpectedValueExceptions. Should we catch those or let them through? We should at least document them in the method docs with the current patch. I would suggest to catch it and return 'sites/default'?

  2. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -314,7 +314,7 @@ public static function findSitePath(Request $request, $require_settings = TRUE)
    @@ -810,7 +810,7 @@ protected function initializeRequestGlobals(Request $request) {
    
    @@ -810,7 +810,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 = $http_protocol . '://' . $request->getHost();
    

    Same here. In D7 we returned 400 Bad request response, are we doing that now? I think we should have a test case for that.

cilefen’s picture

Assigned: Unassigned » cilefen
cilefen’s picture

Assigned: cilefen » Unassigned
Status: Needs work » Needs review
StatusFileSize
new1.32 KB
new2.2 KB

@klausi Thank you for the review. I think that if a request is this malformed, there should be no fallback.

klausi’s picture

Status: Needs review » Needs work
StatusFileSize
new620 bytes

This patch does not fix the DoS issue. I tested with the attached script.

Before the patch (D8 dev from today): 10 seconds response time with 100% CPU
With the patch from #23: 8 seconds with 100% CPU
Drupal 7: 0.006 seconds

So there is still a bug somewhere.

mpdonadio’s picture

Status: Needs work » Needs review
StatusFileSize
new2.31 KB
new583 bytes

#23 doesn't take into account hosts running on ports other than 80. Patch attached, but still shows ~6sec requests w/ the @klausi script.

Setting to Needs Review just so tests run.

klausi’s picture

My gut feeling is that throwing the exception just triggers the theme system which then tries to make sense of the overlong, invalid HTTP host eating up lots of CPU cycles.

Drupal 7 simply stops execution in drupal_environment_initialize():

if (!drupal_valid_http_host($_SERVER['HTTP_HOST'])) {
  // HTTP_HOST is invalid, e.g. if containing slashes it may be an attack.
  header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request');
  exit;
}
mpdonadio’s picture

If I just throw in

--- a/core/vendor/symfony/http-foundation/Symfony/Component/HttpFoundation/Request.php
+++ b/core/vendor/symfony/http-foundation/Symfony/Component/HttpFoundation/Request.php

@@ -1166,6 +1166,10 @@ public function getHost()
             }
         }
 
+        if (strlen($host) > 1000) {
+          throw new \UnexpectedValueException(sprintf('Invalid Host "%s"', $host));
+        }
+
         // trim and remove port number from host
         // host is lowercase as per RFC 952/2181
         $host = strtolower(preg_replace('/:\d+$/', '', trim($host)));

Then the DOS script runs in about ~170ms. Does this mean that the Symfony update isn't sufficient to protect against the DOS?

mpdonadio’s picture

@klausi, if you change

--- a/core/vendor/symfony/http-foundation/Symfony/Component/HttpFoundation/Request.php
+++ b/core/vendor/symfony/http-foundation/Symfony/Component/HttpFoundation/Request.php
@@ -1173,7 +1173,7 @@ public function getHost()
         // as the host can come from the user (HTTP_HOST and depending on the configuration, SERVER_NAME too can come from
         // check that it does not contain forbidden characters (see RFC 952 and RFC 2181)
         // use preg_replace() instead of preg_match() to prevent DoS attacks with long host names
-        if ($host && '' !== preg_replace('/(?:^\[)?[a-zA-Z0-9-:\]_]+\.?/', '', $host)) {
+        if (TRUE) {
             throw new \UnexpectedValueException(sprintf('Invalid Host "%s"', $host));
         }

to force the exception, your test script executes in about 150ms.

Status: Needs review » Needs work

The last submitted patch, 25: port_http_host_header-2304949-25.patch, failed testing.

cilefen’s picture

@klausi I am trying to do it by-the-book with that exception. It is interesting that it may not actually help.

This:

header($request->server->get('SERVER_PROTOCOL' . '400 Bad Request'));

is preferred over

header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request');
cilefen’s picture

@mpdonadio: Could you try:

header($request->server->get('SERVER_PROTOCOL' . '400 Bad Request'));
exit();

... at both places with the try/catch in DrupalKernel?

mpdonadio’s picture

The header() thing didn't seem to make any difference. We should profile this to see where the delay is coming from.

cilefen’s picture

Issue tags: +Needs Profiling; Performance
mpdonadio’s picture

Two more data point on this before profiling,

1. I wasn't totally accurate in #32.

--- a/core/lib/Drupal/Core/DrupalKernel.php
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -319,7 +319,8 @@ public static function findSitePath(Request $request, $require_settings = TRUE)
       $http_host = $request->getHost();
     }
     catch (\UnexpectedValueException $exception) {
-      throw new BadRequestHttpException(t('Bad request'));
+      header($request->server->get('SERVER_PROTOCOL' . '400 Bad Request'));
+      exit();
     }
 
     $sites = array();
@@ -824,7 +825,8 @@ protected function initializeRequestGlobals(Request $request) {
         }
       }
       catch (\UnexpectedValueException $exception) {
-        throw new BadRequestHttpException(t('Bad request'));
+        header($request->server->get('SERVER_PROTOCOL' . '400 Bad Request'));
+        exit();
       }
 
       $base_url = $base_root;

does not result in the 400 Bad Request being generated. The output from the script is a 404 Not Found page where all of the URLs are using the spoofed HOST from the script.

2. The delay does not seem to depend on the length of the spoofed hostname. If I edit the DOS script above to make it a 10 char spoofed hostname, it still takes 6 seconds to run.

David_Rothstein’s picture

Wouldn't the correct code actually be:

header($request->server->get('SERVER_PROTOCOL') . ' 400 Bad Request');

rather than:

header($request->server->get('SERVER_PROTOCOL' . '400 Bad Request'));

?

(note the parentheses)

cilefen’s picture

@David_Rothstein that's what happens when I code in comment boxes

larowlan’s picture

We're actively working to remove exit from core, can this be done without adding new calls?

berdir’s picture

Ok, so we figured out two things:

a) To be vulnerable to this in 8.x, you need a sites.php (it can be empty), as otherwise, we don't do the multisite check.
b) Symfony does *not* include the same validation spectrum as https://api.drupal.org/api/drupal/includes%21bootstrap.inc/function/drup..., it does *not* validate the length or amounts of ./: in any way.

We can of course fix this just in Drupal, as the meaning of ./: is somewhat custom to Drupal/multisite behavior. But we should probably check with symfony just to be sure. There is no information on https://github.com/symfony/symfony/pull/11828 what exactly they are preventing from.

cilefen’s picture

cilefen’s picture

Issue summary: View changes
cilefen’s picture

We could handle this in Drupal with a forward-port of drupal_valid_http_host() to a method on DrupalKernel, and call it before getHost(). If anyone knows of a better way, speak up!

cilefen’s picture

@larowlan, et al. We will try not to introduce another exit(). BadRequestHttpException does some more processing and that later results in a fatal because the hostname isn't found. It may be possible to create a custom Response instead. I am hoping that would end execution, in order to avoid the fatal.

mpdonadio’s picture

Status: Needs work » Needs review
StatusFileSize
new2.5 KB
new786 bytes

Better version of patch in #25 to account for HTTP running on something other than port 80, and HTTPS running on something other than port 443.

larowlan’s picture

  1. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -314,7 +315,12 @@ public static function findSitePath(Request $request, $require_settings = TRUE)
    +      throw new BadRequestHttpException(t('Bad request'));
    
    @@ -810,7 +816,21 @@ protected function initializeRequestGlobals(Request $request) {
    +        throw new BadRequestHttpException(t('Bad request'));
    

    Exceptions should not use t()

  2. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -810,7 +816,21 @@ protected function initializeRequestGlobals(Request $request) {
    +        $base_root = $http_protocol . '://' . $request->getHost();
    ...
    +      // Check for non-default port numbers, and append if needed.
    +      $port = $request->getPort();
    +      if ($http_protocol == 'https' && $port != 443) {
    +        $base_root .= ':' . $port;
    +      }
    +      elseif ($http_protocol == 'http' && $port != 80) {
    +        $base_root .= ':' . $port;
    +      }
    

    $request->getHttpHost() handles all of this logic for us.

mpdonadio’s picture

StatusFileSize
new2.89 KB
new1.79 KB
new2.09 KB

Thanks for the input. Used $request->getSchemeAndHttpHost() to simplify things more. Also added the @throw links to the docblocks.

cilefen’s picture

Status: Needs review » Needs work
Issue tags: -Needs Profiling; Performance +Needs tests

Because, Request::getHost() does not check for the number of periods or colons in the hostname, Drupal is still vulnerable without some more validation. @Berdir and I agree that we need to add a method to DrupalKernel, something like the following. This is my idea for the method, which is based on drupal_valid_http_host() from D7, but without the preg_match, which is redundant to getHost(). We would want to throw an exception in this method or in the calling code if this method does not return TRUE:

  /**
   * Validates a hostname length.
   *
   * @param string $host
   *   A hostname.
   *
   * @return
   *  TRUE if only containing valid characters, or FALSE otherwise.
   */
  protected static function validateHostname($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 conf_path().
    && substr_count($host, '.') <= 100
    && substr_count($host, ':') <= 100;
  }
mpdonadio’s picture

Assigned: Unassigned » mpdonadio
Issue summary: View changes

I'm going to start work on this based on the patch in #45.

cilefen’s picture

StatusFileSize
new2.88 KB

@mpdonadio: this is an effort handle it at a low level. Sorry, I know you are working on this but I thought about it all day.

cilefen’s picture

Status: Needs work » Needs review

The last submitted patch, sec-D7-conf-path-dos-105258-23.patch, failed testing.

berdir’s picture

I don't think we even call drupal_bootstrap() anymore in the normal bootstrap?

I think it is OK if we do that check specifically before the multisite lookup, the DOS attack is specific do that, so it's OK to do the validation specific to that? Then we also don't have to call the validation method if there is no sites.php, as we don't really care about crazy hostnames then.

cilefen’s picture

StatusFileSize
new2.42 KB

@Berdir - Thank you. More like this?

cilefen’s picture

StatusFileSize
new2.42 KB

I mean ... this

berdir’s picture

Yes, something like that I think. Now we need to figure out how to test this, web test won't work, maybe a unit test? But that won't work as long as findSitePath() calls functions like drupal_valid_test_ua().

cilefen’s picture

Issue summary: View changes
klausi’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -314,7 +314,10 @@ public static function findSitePath(Request $request, $require_settings = TRUE)
    +    if (self::validateHostnameLength($http_host) == FALSE) {
    

    This new method should always be called in some environment initialization method. That way we can make sure that such a malicious request is always aborted, regardless of multisite configuration or not. Drupal should just stop bootstrapping and throw a 400 bad request response. And then we can also reliably test this with a web test for example. This is also the way it is handled in Drupal 7.

  2. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -810,7 +813,7 @@ protected function initializeRequestGlobals(Request $request) {
    -      $base_root = $http_protocol . '://' . $request->server->get('HTTP_HOST');
    +      $base_root = $http_protocol . '://' . $request->getHost();
    

    I assume this is wrong because you do not include mpdonadio's changes regarding port numbers?

  3. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -1249,4 +1250,23 @@ protected function classLoaderAddMultiplePsr4(array $namespaces = array()) {
    +   * @return bool|void
    +   *   TRUE if the length is appropriate, or FALSE otherwise.
    

    void? Nope, this is just boolean.

  4. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -1249,4 +1250,23 @@ protected function classLoaderAddMultiplePsr4(array $namespaces = array()) {
    +    // in conf_path().
    

    conf_path() does not exist in D8, you should point to ->findSitePath().

cilefen’s picture

@klausi: Thank you for reviewing. This is just 3 and 4. @mpdonadio: Would you be kind enough to do 2?

cilefen’s picture

StatusFileSize
new2.42 KB
new729 bytes
klausi’s picture

Oh, I overlooked one of Berdir's previous comments. We should definitely not ignore crazy host names and always bail out early, so that there are no subsequent security issues with any code that makes use of the Host header.

mpdonadio’s picture

Status: Needs work » Needs review
StatusFileSize
new201 bytes
new200 bytes
new186 bytes
new2.45 KB
new4.65 KB

OK, here is a first pass, but it is incomplete. There is a slight deviation from @cilefen and I discussed, but I think this is better.

There are two places we need to do the hostname checking, DrupalKernel::findSitePath (because of the multisite negotiation), and either ::preHandle or ::initializeRequestGlobals because the global $base_url gets set there, and getHost() gets called. I only modified the first.

We had originally thought about a new exception for the length problem, but that didn't make sense when I added the catch.

I attached three bash scripts. test1 has a long spoofed host, and will generate a proper 400 response. test2 has a spoofed Host under the character limit, and will generate a proper 200 (which is expected). test2 has an invalid spoofed host, and will generate a proper 400 response. For all three, you should have an empty sites/sites.php so the patch gets triggered.

Looking for input on two things.

1. Is this approach correct?

2. If so, is there a better way to generate the 400 that can avoid the exit.

If we think this approach is OK, then we need to figure out where to do the catch when there isn't a sites.php.

Setting Needs Review b/c I want to see the testbot output.

catch’s picture

Why not do the bail out directly in findSitePath() since that's where the DoS is?

For $base_url I think that's not relevant here given #2221699: HTTP_HOST header cannot be trusted was bumped to critical.

larowlan’s picture

  1. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -206,7 +207,13 @@ public static function createFromRequest(Request $request, $class_loader, $envir
    +      exit;
    

    So lower in this same function we call $response->prepare()->send() to redirect to install.php but don't call exit.
    Is this different because it's not a redirect?

  2. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -1249,4 +1272,29 @@ protected function classLoaderAddMultiplePsr4(array $namespaces = array()) {
    +  }
    

    nitpick: missing new line between the }'s

mpdonadio’s picture

StatusFileSize
new2.41 KB
new462 bytes
new3.6 KB

@cilefen's latest patch w/ better handling for non-standard port numbers. interdiff-57-62.txt shows this.

interdiff-48-62.txt just shows what the differences to my approach are.

mpdonadio’s picture

@larowlan, I assume #62 was looking at port_http_host_header-2304949-48.patch...

Yeah, the exit is there b/c without it Drupal went through the bootstrap process and rendered out a page, when all I wanted was a simple error code to go back to clients as this is an exceptional case. I see it an analogous to the fast 404 handling in Drupal 7. Is there a better way / place to catch this?

@catch, I assume #61 was to me, also. I did a try/catch in createFromRequest() instead of a direct abort from findSitePath() as it seemed the more better way to do it from a semantic perspective of initializing from a request object and getting that particular exception.

I am deferring to everyone else as to how to proceed here... And sorry for the confusion here; I must have had an old tab open and forgot to refresh when went to post my patch.

klausi’s picture

Status: Needs review » Needs work
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -892,9 +894,7 @@ protected function initializeCookieGlobals(Request $request) {
-      // HTTP_HOST can be modified by a visitor, but has been sanitized already
-      // in DrupalKernel::bootEnvironment().

This comment gives a pretty good hint where we could do the sanitization. Let's keep the comment and move the validation there?

I can confirm that the patch fixes the DoS issue with the script I posted above. The response is HTTP/1.1 500 Internal Server Error, which is wrong, this should be 400 Bad request, because the server did nothing wrong.

And we need a test case.

mpdonadio’s picture

Working on this, starting from the patch in #63.

mpdonadio’s picture

StatusFileSize
new6.69 KB
new5.14 KB

This adds the check per @klausi above, and attempts to add an event subscriber to generate a proper 400 Bad Request.

The exception doesn't get caught. Testing with AccessDeniedHttpException and NotFoundHttpException don't work either, so I think I am missing a key concept for how the event subscribers work.

Advice / guidance appreciated.

znerol’s picture

Summary from discussion on IRC: It has been proposed to move the check into an early middleware. However, the container is initialized after the site path has been determined and therefore a check at this point in time is not effective.

It follows that the HTTP_HOST header needs to be validated before the container is initialized, and consequently a response needs to be generated without the help of any container services.

znerol’s picture

Also note that Symfony Request::getHost() throws an UnexpectedValueException if validation fails. Therefore we probably should just do the same.

mpdonadio’s picture

Status: Needs work » Needs review
StatusFileSize
new7.31 KB
new659 bytes

Here is a temporary patch that will give us a starting point for making tests. It's mostly evil, but it will generate a 400 for bad Host headers. Just settings Needs Review for testbot.

Additional notes from IRC:

core/rebuild.php doesn't go through DrupalKernel::createFromRequest(), so the hostname validation may need to be moved back to DrupalKernel::findSitePath() (it is protected by default). We should also audit the entry paths to make sure there aren't any other exceptions.

One idea to do this properly is to do the check and return a NullKernel with an empty handle() method instead of using exceptions. The same thing could be done with the install redirect.

berdir’s picture

There is no way we can implement a reliable web test here? a) this only works if you have a sites.php (Ok, if we do the validation earlier, then this is not needed, but there's still b)) and b) Only if you provide an invalid host name, which doesn't work for common vhost setups?

cilefen’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll
mpdonadio’s picture

Assigned: mpdonadio » Unassigned
gaurav.goyal’s picture

Issue tags: -Needs reroll
StatusFileSize
new7.26 KB

Patch rerolled.

gaurav.goyal’s picture

Status: Needs work » Needs review
mpdonadio’s picture

Assigned: Unassigned » mpdonadio
Status: Needs review » Needs work

I messed something up in #70 (I think I generated the patch w/o rebasing first). I am going to reroll starting from #67, possibly with two or more ideas about how to proceed.

I had thoughts about testing, but can't remember what they were. Maybe they will come back to me as I work on this...

mpdonadio’s picture

StatusFileSize
new5.61 KB
new3.01 KB

Somewhere between port_http_host_header-2304949-62.patch and now, I think I missed a rebase when I generated a patch, and some extra code got in. The attached fixes this.

I think this is our best (least bad?) approach.

It generates a quick 400 with my test scripts, and it will do the detection for both multisite and normal.

The check is done in DrupalKernel.php::findSitePath() as that is the entry point from rebuild.php, should someone accidentally expose this. Otherwise, I think directly after the static::bootEnvironment(); in DrupalKernel.php::createFromRequest() would be more logical place.

The check throws BadRequestHttpException(), which is the Symfony 400, but index.php and rebuild.php catches HttpException. This should future proof things in case Symfony barfs before we have a kernel in place.

My questions are

-- Do we want this protection in rebuild.php? If not, I think moving the code to DrupalKernel.php::createFromRequest() is better.
-- Do we want this protection in authorize.php also? If so, we need the logic there.
-- Any ideas on if this is actually testable?

mpdonadio’s picture

Status: Needs work » Needs review

Apparently, I forgot to change the status...

cilefen’s picture

+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -293,12 +294,28 @@ public function __construct($environment, $class_loader, $allow_dumping = TRUE)
+    // @todo Adjust per resolution to https://github.com/symfony/symfony/issues/12349

We could consider making a pull request for this.

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -293,12 +294,28 @@ public function __construct($environment, $class_loader, $allow_dumping = TRUE)
    +    // Validate the hostname.  $request->getHost can also throw a
    +    // UnexpectedValueException if it detects a bad hostname, but it does not
    +    // validate the length.
    +    // @todo Adjust per resolution to https://github.com/symfony/symfony/issues/12349
    +    try {
    +      $http_host = $request->getHost();
    +      if (self::validateHostnameLength($http_host) == FALSE) {
    +        throw new \UnexpectedValueException('Bad hostname');
    +      }
    +    }
    +    catch (\UnexpectedValueException $e) {
    +      throw new BadRequestHttpException('Bad hostname');
    +    }
    +
    

    I wonder whether we should move this code a little bit down, so that this validation check is done directly before using the host ... its a bit odd to have that before the simpletest check.

  2. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -809,8 +826,7 @@ protected function initializeRequestGlobals(Request $request) {
    -      $http_protocol = $request->isSecure() ? 'https' : 'http';
    -      $base_root = $http_protocol . '://' . $request->server->get('HTTP_HOST');
    +      $base_root = $request->getSchemeAndHttpHost();
    

    <3 but a little bit out of scope?

  3. +++ b/core/rebuild.php
    @@ -25,7 +26,16 @@
    +  $code = $e->getStatusCode();
    +  http_response_code($code);
    +  print Response::$statusTexts[$code];
    +  exit;
    
    +++ b/index.php
    @@ -24,6 +26,11 @@
    +catch (HttpException $e) {
    +  $code = $e->getStatusCode();
    +  http_response_code($code);
    +  print Response::$statusTexts[$code];
    +}
    

    I think you should do rather create a response object and call $response->send() instead

znerol’s picture

Re #77

-- Do we want this protection in rebuild.php? If not, I think moving the code to DrupalKernel.php::createFromRequest() is better.
-- Do we want this protection in authorize.php also? If so, we need the logic there.
-- Any ideas on if this is actually testable?

Regarding whether or not rebuild.php should be protected or not, it was argued in IRC that this script should not be accessible in production anyway (because that would lead to all sorts of DoS opportunities). The rebuild script can be disabled by adding $settings['rebuild_access'] = FALSE; to settings.php. However, note that before being able to read settings, the rebuild script needs to invoke DrupalKernel::findSitePath() - a classic chicken-egg problem. Therefore it is necessary to protect rebuild.php.

Also note that findSitePath() is always called - even for sites which do not use the multisite functionality. It is the only way to actually find the proper settings.php. Therefore the concerns expressed against findSitePath() are moot IMHO (it seems to me that #56 is not so well-informed in this regard). Keeping it there will automatically protect authorize.php, rebuild.php and will work for virtually any crazy way to set up a working DrupalKernel.

Instead of putting the whole logic into findSitePath() it might be feasible to extract it to a public static validateRequest(Request $request) method in DrupalKernel[Interface] and then just invoke that method from within findSitePath(). This might also make it possible to cover that method with a unit-test, because it resolves the problem pointed out by #54 (i.e. a unit test simply could test validateRequest() instead of the whole findSitePath() method.

+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -892,9 +908,7 @@ protected function initializeCookieGlobals(Request $request) {
-      // HTTP_HOST can be modified by a visitor, but has been sanitized already
-      // in DrupalKernel::bootEnvironment().
-      if ($cookie_domain = $request->server->get('HTTP_HOST')) {
+      if ($cookie_domain = $request->getHost()) {
         // Strip leading periods, www., and port numbers from cookie domain.
         $cookie_domain = ltrim($cookie_domain, '.');
         if (strpos($cookie_domain, 'www.') === 0) {

getHost() always returns the hostname without the port number, thus remove the code which strips some lines further down.

Re #80

a little bit out of scope?

I don't think so. This comes from #23. In my understanding HTTP_HOST should not be accessed directly anywhere throughout our codebase. This patch removes all occurrences from production code.

mpdonadio’s picture

OK, another patch...

#80.1: If this code is being called on every request don't we want to make sure it is also called for every simpletest request? That is why I put it at the top.

#80.2: As mentioned above, #23 made the change to not access HTTP_HOST directly. This change was introduced in #25 because the code below it needs the port number.

#80.3: I put this in. It may be overkill, but calling ->prepare() since we are returning a 4xx code may not be a bad idea.

#81.1: I made a public validateRequest() that returns a boolean. Added tests to DrupalKernelTest that i think cover all the bases, and they pass locally for me. Left the throw BadRequestHttpException in findSitePath().

#81.2: Removed port logic for from the cookie setup code.

Comment on validateHostnameLength(). This was based on the Drupal 7's drupal_valid_http_host(). These limits could possibly be tightened up per RFC1035, which says 255 octets for a hostname (that count includes the implicit trailing dot), and we may not need the colon detection since ->getHost doesn't seem to include that?

mpdonadio’s picture

StatusFileSize
new8.54 KB
new5.03 KB

It would help to attach the files...

The last submitted patch, 77: port_http_host_header-2304949-77.patch, failed testing.

znerol’s picture

  1. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -293,12 +294,18 @@ public function __construct($environment, $class_loader, $allow_dumping = TRUE)
    +    if (self::validateHostname($request) !== TRUE) {
    

    Use static:: instead of self::. See docs on late static binding.

  2. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -293,12 +294,18 @@ public function __construct($environment, $class_loader, $allow_dumping = TRUE)
    +    if (self::validateHostname($request) !== TRUE) {
    
    @@ -1249,4 +1253,50 @@ protected function classLoaderAddMultiplePsr4(array $namespaces = array()) {
    +      if (self::validateHostnameLength($http_host) == FALSE) {
    

    Use the same comparison operators in both places.

  3. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -293,12 +294,18 @@ public function __construct($environment, $class_loader, $allow_dumping = TRUE)
    +      throw new BadRequestHttpException('Bad hostname');
    

    The message of the exception is never used, therefore it is not very useful to set it here.

  4. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -892,16 +898,14 @@ protected function initializeCookieGlobals(Request $request) {
    +        // Restore one leading period per RFC 2109
    

    Do not mention that RFC here, it is outdated. In fact we add the leading dot in order to maximize browser-compatibility, see #2347877-17: Move DrupalKernel::initializeCookieGlobals() into a SessionConfiguration service. If in doubt do not add the comment in order to prevent bike-shedding about that.

  5. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -1249,4 +1253,50 @@ protected function classLoaderAddMultiplePsr4(array $namespaces = array()) {
    +  protected static function validateHostnameLength($host) {
    

    I tend to think that this could be inlined into the new validHostname function. On the other hand I also see the value in keeping it analogous to the D7 function. Other opinions?

  6. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -1249,4 +1253,50 @@ protected function classLoaderAddMultiplePsr4(array $namespaces = array()) {
    +      if (self::validateHostnameLength($http_host) == FALSE) {
    

    Use static.

  7. +++ b/core/modules/system/src/Tests/DrupalKernel/DrupalKernelTest.php
    @@ -9,6 +9,7 @@
    +use Drupal\Core\Test\TestKernel;
    

    That does not seem to be used.

  8. +++ b/core/modules/system/src/Tests/DrupalKernel/DrupalKernelTest.php
    @@ -153,4 +154,44 @@ public function testCompileDIC() {
    +  /**
    +   * Tests hostname validate.
    +   */
    +  public function testValidateHostame() {
    

    Great that this is finally testable. Note that it might be worthwhile to keep the test-scenarios inline with the ones from D7 in order to simplify maintenance. See BootstrapIPAddressTestCase::testIPAddressHost().

  9. +++ b/index.php
    @@ -24,6 +26,12 @@
    +  $code = $e->getStatusCode();
    +  $content = Response::$statusTexts[$code];
    +  $response = new Response($content, $code);
    

    In the D7 version, the response has no body (i.e. there is no print in the respective part of drupal_environment_initialize(). Just use $response = new Response('', $e->getStatusCode());.

mpdonadio’s picture

StatusFileSize
new1.81 KB

I think keeping validateHostnameLength() as a separate function makes the code read better, especially with the try/catch logic that is needed in validateHostname.

And before I post another version of the patch, keeping the validation in DrupalKernel::findSitePath() will protect index.php, rebuild.php, and authorize.php (our only entry points other than install.php, which doesn't need protection). We didn't fully decide whether we need to generate a 400 in rebuild.php and authorize.php. Do we need/want to generate a 400 from these when validation fails? The change would be similar to the attached.

klausi’s picture

Status: Needs review » Needs work
+++ b/core/modules/system/src/Tests/DrupalKernel/DrupalKernelTest.php
@@ -153,4 +154,44 @@ public function testCompileDIC() {
+  /**
+   * Tests hostname validate.
+   */
+  public function testValidateHostame() {

why isn't this a phpunit test if you are only testing the validateHostname() method? It would be nice to have test coverage for the added code in at least index.php by writing a web test that issues a malicious request with an invalid host name, but would be a bit tricky to implement since I think you cannot use cURL and would have to fiddle with PHP sockets yourself.

I would have liked to see a more generic request validation phase in the DrupalKernel, since we will most likely encounter further security issues on the request object during the D8 release cycle. It feels a bit hacky to just always tuck them onto the place where we find vulnerabilities. But for the sake of making progress here let's go with this approach and postpone that.

Otherwise looks good to me, please fix the test to use phpunit (or add a comment why you can't) and implement znerol's feedback, which looks valid to me.

mpdonadio’s picture

Status: Needs work » Needs review
StatusFileSize
new9.7 KB
new7.03 KB

OK, all of the feedback from #85 is done, but I left validateHostnameLength() as a separate function as I think this make the code read better.

Added try/catch logic to rebuild.php and authenticate.php so a 400 will be generated. I can back this out if desired.

Reverted core/modules/system/src/Tests/DrupalKernel/DrupalKernelTest back to HEAD

Created \Drupal\Tests\Core\DrupalKernel\ValidateHostnameTest. Didn't see a better place to put unit tests for DrupalKernel. Mirrors the D7 version.

Tomorrow I'll make a followup issue about a generic request validation phase, and try to summarize it.

berdir’s picture

@klausi: A web test is impossible here, for the simple reason as that will never work in a vhost environment as you never get to the right vhost, AFAIK.

mpdonadio’s picture

From Name-based Virtual Host Support:

With name-based virtual hosting, the server relies on the client to report the hostname as part of the HTTP headers. Using this technique, many different hosts can share the same IP address.

So, when we spoof the Host header in the test, we may never arrive at the proper virtual host. The only situation where this may work is if qa.drupal.org doesn't do name-based virtual hosting, but that would really complicate people running tests themselves and I have never seen a reliable way to detect from PHP whether name-based virtual hosting is actually being used.

klausi’s picture

Status: Needs review » Needs work

I see, so a web test is out of the question. I guess testing DrupalKernel::createFromRequest() is also impossible because it is tied to global state much ... so the pure unit test seems to be all that we can do.

  1. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -293,12 +294,18 @@ public function __construct($environment, $class_loader, $allow_dumping = TRUE)
    +   * @throws BadRequestHttpException
    

    needs to be fully namespaced. Should have a description like "In case the host name in the request is invalid."

  2. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -293,12 +294,18 @@ public function __construct($environment, $class_loader, $allow_dumping = TRUE)
    +    if (static::validateHostname($request) !== TRUE) {
    

    "!== TRUE" is harder to read than simply "=== FALSE".

  3. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -1249,4 +1252,50 @@ protected function classLoaderAddMultiplePsr4(array $namespaces = array()) {
    +   * @param Request $request
    

    the Request class needs to be fully namespaced in the comment.

  4. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -1249,4 +1252,50 @@ protected function classLoaderAddMultiplePsr4(array $namespaces = array()) {
    +      if (static::validateHostnameLength($http_host) !== TRUE) {
    

    same here with the "=== FALSE"

  5. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -1249,4 +1252,50 @@ protected function classLoaderAddMultiplePsr4(array $namespaces = array()) {
    +        throw new \UnexpectedValueException('Bad hostname');
    

    you can just return FALSE here and move that check out of the try/catch block?

  6. +++ b/core/tests/Drupal/Tests/Core/DrupalKernel/ValidateHostnameTest.php
    @@ -0,0 +1,63 @@
    +    $request = Request::createFromGlobals();
    

    you don't need to create the request from globals. Just use the constructor of the request class. Then you also don't need to mess around with $_SERVER.

Almost ready otherwise, good work!

mpdonadio’s picture

Status: Needs work » Needs review
StatusFileSize
new9.5 KB
new3.58 KB

This should address #92:1-6

znerol’s picture

+++ b/core/authorize.php
@@ -54,9 +54,16 @@ function authorize_access_allowed() {
+catch (HttpException $e) {

While manual testing I noted that the exception is not caught here. This is due to a missing use-statement. Same for rebuild.php. Also note that there is HttpExceptionInterface, I suggest type-hinting against that.

I tried to reproduce the results with the script from #24 for the following paths:

core/authorize.php
core/install.php
core/modules/statistics/statistics.php
core/rebuild.php
index.php

Note that statistics.php and install.php deliver a zero-length (no body) page with status code 500. I think this is okay.

Regrettably I do not see much of a difference between a proper request and a forged one for HEAD. I guess that's probably because I use an SSD. So maybe somebody with a good old hard-disk could run the test script against the new patch and post the results?

klausi’s picture

@znerol: Are you missing the multi site configuration file mentioned in #38 to exploit the DoS?

znerol’s picture

StatusFileSize
new1.09 KB
new1.41 KB
new1.46 KB

@klausi indeed.

I've slightly adapted the test-script and inserted a call to wget for each URL in order to have a baseline. The patch provides an effective protection for all front-controller scripts. Note the simpletest mock-frontcontrollers (http.php and https.php) are already protected by the call to drupal_valid_test_ua() in TestKernel::createFromRequest().

Attached is the modified dos script as well as the output produced by running it on my machine.

So, this is ready except for the missing use statements and the usage of HttpExceptionInterface instead of HttpException in the catch clauses (#93).

mpdonadio’s picture

StatusFileSize
new9.93 KB
new2.07 KB

Replaced HttpException with HttpExceptionInterface.

There was a small conflict when I rebased this morning; interdiff is between my git branches after the manual conflict resolution.

klausi’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs tests

Looks good to me now, thanks!

Crell’s picture

+++ b/core/tests/Drupal/Tests/Core/DrupalKernel/ValidateHostnameTest.php
@@ -0,0 +1,53 @@
+  public function testValidateHostame() {
+    // Verifies that DrupalKernel::validateHostname() prevents invalid
+    // characters.
+    $this->assertFalse($this->validHttpHost('security/.drupal.org:80'), 'HTTP_HOST with / is invalid');
+    $this->assertFalse($this->validHttpHost('security\\.drupal.org:80'), 'HTTP_HOST with \\ is invalid');
+    $this->assertFalse($this->validHttpHost('security<.drupal.org:80'), 'HTTP_HOST with &lt; is invalid');
+    $this->assertFalse($this->validHttpHost('security..drupal.org:80'), 'HTTP_HOST with .. is invalid');
+    // Verifies that host names are shorter than 1000 characters.
+    $this->assertFalse($this->validHttpHost(str_repeat('x', 1001)), 'HTTP_HOST with more than 1000 characters is invalid.');
+    $this->assertFalse($this->validHttpHost(str_repeat('.', 101)), 'HTTP_HOST with more than 100 subdomains is invalid.');
+    $this->assertFalse($this->validHttpHost(str_repeat(':', 101)), 'HTTP_HOST with more than 100 portseparators is invalid.');
+  }
+
+  /**
+   * Validates a spoofed HTTP_HOST.
+   *
+   * @param $hostname
+   *   The hostname to use for the HTTP_HOST.
+   *
+   * @return bool
+   *   TRUE if the spoofed hostname is valid, FALSE otherwise.
+   */
+  protected function validHttpHost($hostname) {
+    $server = array('HTTP_HOST' => $hostname);
+    $request = new Request(array(), array(), array(), array(), array(), $server);
+    return DrupalKernel::validateHostname($request);
+  }

This should be done with a @dataProvider, no?

mpdonadio’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new10.08 KB
new3.38 KB

@timplunkett pretty much did this quicker than me when I asked about DataProviders in IRC. The patch is essentially his. He also added the coverage for returning TRUE, I added in the IPv6 loopback test from BootstrapIPAddressTestCase.

Setting Needs Review so TestBot runs this immediately.

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -1255,4 +1259,51 @@ protected function classLoaderAddMultiplePsr4(array $namespaces = array()) {
    +    if (static::validateHostnameLength($http_host) === FALSE) {
    +      return FALSE;
    +    }
    ...
    +    return TRUE;
    

    you could also just do a return static::validateHostnameLength($http_host) and be done with it

  2. +++ b/core/rebuild.php
    @@ -25,7 +27,15 @@
    +  $response = new Response('', $e->getStatusCode());
    +  $response->prepare($request)->send();
    
    +++ b/index.php
    @@ -24,6 +26,10 @@
    +  $response = new Response('', $e->getStatusCode());
    +  $response->prepare($request)->send();
    

    Are we 100% sure that we don't want to use $e->getMessage()?

mpdonadio’s picture

#100-1, I left it that was for a cleaner patch for when/if Synfony handles the hostname length internally. I change this if desired.

#100-2, in #88 we removed the exception message, which may or may not line up with the status code we are sending back.

$code = $e->getStatusCode();
$content = Response::$statusTexts[$code];
$response = new Response($content, $code);
$response->prepare($request)->send();

This would just probably be better if we want content.

znerol’s picture

Status: Needs review » Reviewed & tested by the community

Are we 100% sure that we don't want to use $e->getMessage()?

We do not want to emit a body because if we did we'd need to take extra-care with escaping it and whatnot. The status text (Bad request) is already in the status header.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/tests/Drupal/Tests/Core/DrupalKernel/ValidateHostnameTest.php
@@ -0,0 +1,57 @@
+    $data[] = [str_repeat('.', 101), 'HTTP_HOST with more than 100 subdomains is invalid.'];

This test is not testing what you think it is.

$server = ['HTTP_HOST' => str_repeat('.', 101)];
$request = new Request([], [], [], [], [], $server);
$request->getHost();

Throws an invalid value exception. Something like ['HTTP_HOST' => str_repeat('a.', 101)]; is what you want.

mpdonadio’s picture

Per some IRC with @klausi and @alexpott, I am going to rework the tests to diverge from the exact test cases from drupal_valid_http_host() in the Drupal 7 BootstrapIPAddressTestCase::testIPAddressHost, and to the cover the Drupal 8 cases better.

There are two parts of this. One are the bad hostname lengths that we check for in DrupalKernel::validateHostnameLength (which are the real cause of our DOS issues). The others are the exceptions that Request::getHost() will throw.

We are currently testing for one case, which are characters outside of what is defined by RFC 952/2181.

We are not testing the trusted hosts mechanism that the class defines. AFAIK, we don't support this in 8.0.x, but we may want to in the future. Should I add test coverage for these too, while I am at it?

klausi’s picture

No, I think we should only fix the security issue here.

mpdonadio’s picture

Status: Needs work » Needs review
StatusFileSize
new10.51 KB
new2.29 KB

Adjusted tests based on some IRC w/ @klausi and @alexpott.

-- Updated comment to match what Request->getHost() checks for (which is the exception that gets caught in ::validateHostname()
-- Updated the second batch of tests to better reflect real hostnames that we consider invalid in ::validateHostnameLength()
-- Added two tests for valid IPv4 and IPv6 hostnames to make sure they OK with these two methods.

klausi’s picture

Status: Needs review » Reviewed & tested by the community

Updated test cases look good.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

This issue addresses a critical bug and is allowed per https://www.drupal.org/core/beta-changes. Committed 62ee12b and pushed to 8.0.x. Thanks!

  • alexpott committed 62ee12b on 8.0.x
    Issue #2304949 by mpdonadio, cilefen, znerol, klausi, gaurav.goyal,...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

donquixote’s picture

PhpStorm now rightfully complains:
The variable 'request' might have not been defined.

+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;
+}

The IDE is always right.
What to do?

donquixote’s picture

We could fix it like this:

$request = Request::createFromGlobals();
try {
  $kernel = DrupalKernel::createFromRequest($request, $autoloader, 'prod');
  $kernel->prepareLegacyRequest($request);
}
catch (HttpExceptionInterface $e) {
  $response = new Response('', $e->getStatusCode());
  $response->prepare($request)->send();
  exit;
}

It an exception is thrown in Request::createFromGlobals(), we will not catch it. Is this ok?
Maybe Request::createFromGlobals() never throws HttpExceptionInterface kind of exceptions. But it is always nicer to have IDE-proof logic.

znerol’s picture

@donquixote: Please bring that up in #2389811: Move all the logic out of index.php (again).

mpdonadio’s picture

Assigned: mpdonadio » Unassigned

Cleaning up the issues that I am assigned to...

jibran’s picture

+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -1255,4 +1259,51 @@ protected function classLoaderAddMultiplePsr4(array $namespaces = array()) {
+   * @todo Adjust per resolution to https://github.com/symfony/symfony/issues/12349

This has been closed now. How can we remove this @todo now?

mpdonadio’s picture