Problem/Motivation

FinishResponseSubscriber::onRespond() sets some default values for headers.
When setting the values, it specifies the $replace parameter as FALSE, it appears with the intention to not overwrite any headers that have previously been set to the response object's headers.

However, the effect is that $replaces = FALSE instead appends an additional header of the same name, with the specified value.

This may not occur in most cases, assuming:
- modules which alter the headers subscribe with the same or lower priority as the core handler (and consequently execute after the core handler).
- modules set the header value with the parameter $replace == TRUE (which is the default) so that core's value is overwritten.

Steps to reproduce

Create an event subscriber, with a handler for the KernelEvents::RESPONSE event, with an increased priority (>0) that sets one of the following headers:
- X-Content-Type-Options
- X-Frame-Options

The resulting page response will have the default value appended to that of the test subscriber

HTTP/2 200 OK
...
x-ua-compatible: FOO=bar, IE=edge
...

Proposed resolution

Only set the header if a value has not already been set on the request.

Remaining tasks

API changes

Release notes snippet

Issue fork drupal-3214208

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

gapple created an issue. See original summary.

gapple’s picture

Issue summary: View changes
Status: Active » Needs review
gapple’s picture

Issue summary: View changes

I originally expected there to be duplicate headers, e.g.

HTTP/2 200 OK
...
x-ua-compatible: FOO=bar
x-ua-compatible: IE=edge
...

but the actual result is that Symfony concatenates the values into a single header:

HTTP/2 200 OK
...
x-ua-compatible: FOO=bar, IE=edge
...

My understanding is that the browser would treat these equally.

gapple’s picture

StatusFileSize
new4.6 KB

Test only patch

Status: Needs review » Needs work

The last submitted patch, 5: drupal-3214208-testonly.patch, failed testing. View results

gapple’s picture

Status: Needs work » Needs review

Really not sure why the MR test failed - it was successful locally.

joelpittet’s picture

From what I can tell it should be working. I am re-running the test.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

sd123’s picture

This merge request does not fix this bug on my site. I keep getting a duplicate IE=edge entry in the x-ua-compatible header.

gapple’s picture

@sd123 to help determine if core is part of the issue on your site, you can try removing the line $response->headers->set('X-UA-Compatible', 'IE=edge'); from FinishResponseSubscriber, so that core isn't setting / appending the header at all.
Alternately, altering the value set by core instead of removing it would give an indication which order the header values are being set (it's plausible that another module copied the parameters from core as an example, not understanding that it would result in the duplicate header value).

gapple’s picture

It appears that between the time I had last pulled changes and the time I worked on the MR, core updated to replace its use of the deprecated isMasterRequest with the replacement isMainRequest, which can be noted by the testonly patch also showing that no header value was set since it mocks the wrong method 🤦🏻

I've rebased the merge request to 9.4.x, and fixed the test.

gapple’s picture

and bit of a tangent, but X-UA-Compatible can also be definitely removed from core in Drupal 10 (since IE will no longer be supported), and likely removed from Drupal 9 (since Drupal sets a doctype, and consequently setting IE=edge has not affect)

sd123’s picture

Commenting out the line in FinishResponseSubscriber fixes the issue. It obviously removes the whole HTTP X-UA-Compatible header.

I also replaced it with 'IE=edgetest'. The result of doing that, is that I first get the error "X-UA-Compatible HTTP header must have the value IE=edge, was IE=edgetest." after a cache flush. On multiple page visits, the error changes to "X-UA-Compatible HTTP header must have the value IE=edge, was IE=edgetest, IE=edgetest."

Personally, I would not mind dropping any IE features like this. Microsoft already dropped support for their browser and that browser is not used much. I just checked on my site. Last year, only 0.3% of visitors to my site were using IE. Since November 1st 2021 this was only 0.11%.

gapple’s picture

That's... odd...
There may be some underlying issue that's causing the event subscriber to be called twice on the same request (or maybe being called on a cached response that already includes the headers?), which AFAIK shouldn't normally happen. This MR should prevent that double execution from having any effect though (from a glance, the rest of the code in the event listener is already idempotent).

sd123’s picture

StatusFileSize
new7.44 KB

Well, my site is optimized for performance and I use several modules for that. It sounds possible to me that (a combination of) one of these modules or their configuration is triggering this bug. I have attached a list of non-core modules that are enabled on my site. If this can help, I can temporarily disable some of them on my test site to see if that makes a difference.

sd123’s picture

The issue does still appear in the latest 9.3.11 release. Commenting out the line in FinishResponseSubscriber fixes the issue as in the past.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new149 bytes

The Needs Review Queue Bot tested this issue. It either no longer applies to Drupal core, or fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

gapple’s picture

Issue summary: View changes
Status: Needs work » Needs review

MR rebased to 10.1

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs Review Queue Initiative

Very very small change requested.

When I run the tests locally without the fix
testDefaultHeaders passes should it fail?

gapple’s picture

Status: Needs work » Needs review

Fixed the small issue, and removed the X-UA-Compatible from the existing header test, since it's no longer added by core.

testDefaultHeaders() should pass both before and after the change; testExistingHeaders() should only pass after the change.

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

Thank you for the quick follow up!

larowlan’s picture

Adding review credit for @smustgrave

larowlan’s picture

According to the documentation, there is only one possible value for X-Content-Type-Options, and that's nosniff

Can you elaborate on the scenario where you need to set an alternative value?

Similarly for X-FRAME-OPTIONS there's only two options, SAMEORIGIN or DENY

If you switch it to DENY, you will break things like media library embedding of oembed videos.

Can you elaborate on the use-case for that change too?

larowlan’s picture

Status: Reviewed & tested by the community » Needs work

Putting back to needs work, pending response above, however I would expect we'd need to reverse the X-Content-Type-Options changes given that header only takes one possible value.

gapple’s picture

Status: Needs work » Needs review

As I noted in the summary, the current code is unlikely to cause issues in practice since a module that wants to alter the value is likely to execute after core's handler and also replace the value set by core - core setting the $replace parameter is just incorrect for what the code intends to do. Were a module to be prioritized before core's event handler and also set the same value it would also result in a duplicate header, just with the same value twice.

The values set in FinishResponseSubscriber are not changed by this issue (X-Frame-Options: SAMEORIGIN is still the default set by core, as now tested by FinishResponseSubscriberTest::testDefaultHeaders()), the test just sets them to different values to best ensure it is actually testing what's expected.

The headers could just unconditionally set the header values by removing the $replace parameter value (like is done for Content-language just before) - having the conditional just matches what appears to be the intent behind why the $replace parameter was originally set.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new1.94 KB

The Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

This does not mean that the patch needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

gapple’s picture

Status: Needs work » Needs review

Fixed missing parent:setUp() in test that bot flagged.

borisson_’s picture

Status: Needs review » Needs work

I found a super small code nitpick, but this change looks super solid otherwise. +1 for rtbc after that is fixed.
Great job on the test coverage, that looks great!

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

david.muffley made their first commit to this issue’s fork.

david.muffley’s picture

Status: Needs work » Reviewed & tested by the community

Removed the nitpicks from #31 so moving to RTBC.

larowlan’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Bug Smash Initiative

I think we should do #26/ #28 for the x-content-type-options header - i.e just remove the replace variable and set it unconditionally - there's no use-case for anything else per the spec.

fine to self-RTBC after that, will keep an eye out for the change

Akhil Babu made their first commit to this issue’s fork.

Akhil Babu changed the visibility of the branch 11.x to hidden.

Akhil Babu changed the visibility of the branch 3214208-finishresponsesubscriber-could-create to hidden.

akhil babu’s picture

Status: Needs work » Needs review

Added the changes as per #35 and created a new merge request against 11.x branch

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

Appears feedback to only set X-Frame-Options is empty is addressed

  • larowlan committed eb526a83 on 10.2.x
    Issue #3214208 by gapple, Akhil Babu, larowlan, smustgrave:...

  • larowlan committed 208bb878 on 11.x
    Issue #3214208 by gapple, Akhil Babu, larowlan, smustgrave:...

  • larowlan committed 73fb8860 on 10.2.x
    Issue #3214208 by gapple, Akhil Babu, larowlan, smustgrave:...

  • larowlan committed 354e46cb on 11.x
    Issue #3214208 by gapple, Akhil Babu, larowlan, smustgrave:...
larowlan’s picture

Version: 11.x-dev » 10.2.x-dev
Status: Reviewed & tested by the community » Fixed

Compared the two MRs

here's the diff

diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
index 6d93ec91915..6fc54a38c00 100644
--- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -123,9 +123,7 @@ public function onRespond(ResponseEvent $event) {
     // different from the declared content-type, since that can lead to
     // XSS and other vulnerabilities.
     // https://owasp.org/www-project-secure-headers
-    if (!$response->headers->has('X-Content-Type-Options')) {
-      $response->headers->set('X-Content-Type-Options', 'nosniff');
-    }
+    $response->headers->set('X-Content-Type-Options', 'nosniff');
     if (!$response->headers->has('X-Frame-Options')) {
       $response->headers->set('X-Frame-Options', 'SAMEORIGIN');
     }
diff --git a/core/tests/Drupal/Tests/Core/EventSubscriber/FinishResponseSubscriberTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/FinishResponseSubscriberTest.php
index 790c291c26e..a77403daf41 100644
--- a/core/tests/Drupal/Tests/Core/EventSubscriber/FinishResponseSubscriberTest.php
+++ b/core/tests/Drupal/Tests/Core/EventSubscriber/FinishResponseSubscriberTest.php
@@ -125,7 +125,8 @@ public function testExistingHeaders() {
     $finishSubscriber->onRespond($event);
 
     $this->assertEquals(['en'], $response->headers->all('Content-language'));
-    $this->assertEquals(['foo'], $response->headers->all('X-Content-Type-Options'));
+    // 'X-Content-Type-Options' will be unconditionally set by the core.
+    $this->assertEquals(['nosniff'], $response->headers->all('X-Content-Type-Options'));
     $this->assertEquals(['DENY'], $response->headers->all('X-Frame-Options'));
   }

Committed to 11.x and 10.2.x

Made a minor cleanup/grammar commit as follows:

diff --git a/core/tests/Drupal/Tests/Core/EventSubscriber/FinishResponseSubscriberTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/FinishResponseSubscriberTest.php
index a77403daf41..f83951cc85a 100644
--- a/core/tests/Drupal/Tests/Core/EventSubscriber/FinishResponseSubscriberTest.php
+++ b/core/tests/Drupal/Tests/Core/EventSubscriber/FinishResponseSubscriberTest.php
@@ -125,7 +125,7 @@ public function testExistingHeaders() {
     $finishSubscriber->onRespond($event);
 
     $this->assertEquals(['en'], $response->headers->all('Content-language'));
-    // 'X-Content-Type-Options' will be unconditionally set by the core.
+    // 'X-Content-Type-Options' will be unconditionally set by core.
     $this->assertEquals(['nosniff'], $response->headers->all('X-Content-Type-Options'));
     $this->assertEquals(['DENY'], $response->headers->all('X-Frame-Options'));
   }

wim leers’s picture

I bet this also fixed a bug reported against the BigPipe Sessionless module I maintain: #3268665: Security headers are duplicated (X-UA-Compatible, X-Content-Type-Options, X-Frame-Options).

Status: Fixed » Closed (fixed)

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

fgm’s picture

I just noticed this on an up-to-date Drupal 11 site returning this header:

D11 pages X-Frame-Options: SAMEORIGIN, SAMEORIGIN
Files from Apache on the same site X-Frame-Options: SAMEORIGIN
Other non-Drupal sites on the same server X-Frame-Options: deny
Files from Apache on other sites on the same server (no X-Frame-Options header)

This appears to imply that such duplicate headers can still happen with D11 on Apache without such a header in the global config, since the header does not appear on other vhosts on the server, appears once on public files for the D11, and twice on D11 pages.

In our specific case, that happened because a directory block in the vhost already included that header. Not sure Drupal can actually do anything to prevent the double header being emitted.

idebr’s picture

Not sure Drupal can actually do anything to prevent the double header being emitted.

It can, and it does (for X-Content-Type-Options), see https://git.drupalcode.org/project/drupal/-/blob/11.x/.htaccess?ref_type...