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
| Comment | File | Size | Author |
|---|---|---|---|
| #29 | 3214208-nr-bot.txt | 1.94 KB | needs-review-queue-bot |
Issue fork drupal-3214208
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
Comment #3
gappleComment #4
gappleI originally expected there to be duplicate headers, e.g.
but the actual result is that Symfony concatenates the values into a single header:
My understanding is that the browser would treat these equally.
Comment #5
gappleTest only patch
Comment #7
gappleReally not sure why the MR test failed - it was successful locally.
Comment #8
joelpittetFrom what I can tell it should be working. I am re-running the test.
Comment #10
sd123 commentedThis merge request does not fix this bug on my site. I keep getting a duplicate IE=edge entry in the x-ua-compatible header.
Comment #11
gapple@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');fromFinishResponseSubscriber, 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).
Comment #12
gappleIt 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
isMasterRequestwith the replacementisMainRequest, 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.
Comment #13
gappleand bit of a tangent, but
X-UA-Compatiblecan 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 settingIE=edgehas not affect)Comment #14
sd123 commentedCommenting 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%.
Comment #15
gappleThat'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).
Comment #16
sd123 commentedWell, 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.
Comment #17
sd123 commentedThe issue does still appear in the latest 9.3.11 release. Commenting out the line in FinishResponseSubscriber fixes the issue as in the past.
Comment #20
needs-review-queue-bot commentedThe 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.
Comment #21
gappleMR rebased to 10.1
Comment #22
smustgrave commentedVery very small change requested.
When I run the tests locally without the fix
testDefaultHeaders passes should it fail?
Comment #23
gappleFixed the small issue, and removed the
X-UA-Compatiblefrom 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.Comment #24
smustgrave commentedThank you for the quick follow up!
Comment #25
larowlanAdding review credit for @smustgrave
Comment #26
larowlanAccording to the documentation, there is only one possible value for X-Content-Type-Options, and that's
nosniffCan 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?
Comment #27
larowlanPutting 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.
Comment #28
gappleAs 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
$replaceparameter 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
FinishResponseSubscriberare not changed by this issue (X-Frame-Options: SAMEORIGINis still the default set by core, as now tested byFinishResponseSubscriberTest::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
$replaceparameter value (like is done forContent-languagejust before) - having the conditional just matches what appears to be the intent behind why the$replaceparameter was originally set.Comment #29
needs-review-queue-bot commentedThe 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.
Comment #30
gappleFixed missing
parent:setUp()in test that bot flagged.Comment #31
borisson_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!
Comment #34
david.muffley commentedRemoved the nitpicks from #31 so moving to RTBC.
Comment #35
larowlanI 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
Comment #40
akhil babuAdded the changes as per #35 and created a new merge request against 11.x branch
Comment #41
smustgrave commentedAppears feedback to only set X-Frame-Options is empty is addressed
Comment #47
larowlanCompared the two MRs
here's the diff
Committed to 11.x and 10.2.x
Made a minor cleanup/grammar commit as follows:
Comment #49
wim leersI 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).
Comment #51
fgmI just noticed this on an up-to-date Drupal 11 site returning this header:
X-Frame-Options: SAMEORIGIN, SAMEORIGINX-Frame-Options: SAMEORIGINX-Frame-Options: denyThis 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.
Comment #52
idebr commentedIt can, and it does (for X-Content-Type-Options), see https://git.drupalcode.org/project/drupal/-/blob/11.x/.htaccess?ref_type...