Problem/Motivation

In #462950: Mitigate the security risks that come from IE, Chrome and other browsers trying to sniff the mime type we added

  # Disable content sniffing, since it's an attack vector.
  Header always set X-Content-Type-Options nosniff

To the .htaccess file. We also have

    // Prevent browsers from sniffing a response and picking a MIME type
    // different from the declared content-type, since that can lead to
    // XSS and other vulnerabilities.
    // https://www.owasp.org/index.php/List_of_useful_HTTP_headers
    $response->headers->set('X-Content-Type-Options', 'nosniff', FALSE);

in \Drupal\Core\EventSubscriber\FinishResponseSubscriber::onRespond().

It needs to be in both places because we want the nosniff header on files not served by Drupal's index.php, for example, images.

However, if I get the home page of my local dev build I see the following headers:

Cache-Control:must-revalidate, no-cache, private
Connection:Keep-Alive
Content-Encoding:gzip
Content-language:en
Content-Length:1619
Content-Type:text/html; charset=UTF-8
Date:Wed, 22 Feb 2017 09:16:54 GMT
Expires:Sun, 19 Nov 1978 05:00:00 GMT
Keep-Alive:timeout=5, max=100
Server:Apache/2.4.23 (Unix) PHP/7.1.0 LibreSSL/2.2.7
Vary:Accept-Encoding
X-Content-Type-Options:nosniff
X-Content-Type-Options:nosniff
X-Drupal-Cache:MISS
X-Drupal-Cache-Contexts:languages:language_interface route theme url.path url.query_args user.permissions user.roles:authenticated
X-Drupal-Cache-Tags:block_view config:block.block.stark_admin config:block.block.stark_branding config:block.block.stark_local_actions config:block.block.stark_local_tasks config:block.block.stark_login config:block.block.stark_messages config:block.block.stark_page_title config:block.block.stark_tools config:block_list config:system.menu.admin config:system.menu.tools config:system.site config:user.role.anonymous config:user.settings http_response rendered
X-Drupal-Dynamic-Cache:HIT
X-Frame-Options:SAMEORIGIN
X-Generator:Drupal 8 (https://www.drupal.org)
X-Powered-By:PHP/7.1.0
X-UA-Compatible:IE=edge

As you can see the X-Content-Type-Options header is duplicated.

This problem also causes \Drupal\system\Tests\Routing\RouterTest to fail for me locally with:

Fail      Other      RouterTest.php      40 Drupal\system\Tests\Routing\RouterT
    Value 'nosniff,nosniff' is equal to value 'nosniff'.

Which is how I found this problem.

Proposed resolution

I think the we need to fix the .htaccess rule so this does not occur. Because the line in \Drupal\Core\EventSubscriber\FinishResponseSubscriber::onRespond() makes this work for other webservers for every delivered via index.php.

Remaining tasks

User interface changes

None

API changes

None

Data model changes

None

Release notes snippet

The root .htaccess file now unsets the X-Content-Type-Options header before setting it again, to prevent duplicate headers in some configurations of Apache. Site owners should update their .htaccess files with this change to avoid duplicated headers.

Issue fork drupal-2854817

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

alexpott created an issue. See original summary.

alexpott’s picture

Status: Active » Needs review
Issue tags: -7.40 release notes
StatusFileSize
new1.06 KB

Let's see if Apache has mod_headers installed on DrupalCI. The additional test coverage passes locally for me.

Status: Needs review » Needs work

The last submitted patch, 2: 2854817-2.patch, failed testing.

alexpott’s picture

System.Drupal\system\Tests\System\HtaccessTest.testXContentTypeOptions (from System.System)

Failing for the past 1 build (Since Unstable#3083 )
Took 0 ms.
Error Message

fail: [Other] Line 162 of core/modules/system/src/Tests/System/HtaccessTest.php:
Value false is equal to value 'nosniff'.

fail: [Other] Line 169 of core/modules/system/src/Tests/System/HtaccessTest.php:
Value false is equal to value 'nosniff'.

So as suspected mod_headers is not running on DrupalCI. If it was \Drupal\system\Tests\Routing\RouterTest would fail.

alexpott’s picture

If I make the following change:

 # Various header fixes.
 <IfModule mod_headers.c>
   # Disable content sniffing, since it's an attack vector.
-  Header always set X-Content-Type-Options nosniff
+  Header set X-Content-Type-Options nosniff
   # Disable Proxy header, since it's an attack vector.
   RequestHeader unset Proxy
 </IfModule>

RouterTest passes locally for me BUT the 403 case in the new test added by this patch (\Drupal\system\Tests\System\HtaccessTest::testXContentTypeOptions) does not. :(

I guess the question is do Chrome and IE do mime type sniffing on 400s. To me that sounds very very odd.

mile23’s picture

I'm seeing this in 8.3.x because Drupal\system\Tests\Routing\RouterTest never passes locally. It gives this fail message:

Value 'nosniff,nosniff' is equal to value 'nosniff'.
mile23’s picture

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

This patch fixes the test, but not the problem.

Here's what it says in the test result:

x-content-type-options value: nosniff,nosniff which contains nosniff.

It applies to both 8.3.x and 8.4.x.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

wim leers’s picture

wim leers’s picture

BTW, I've had this same problem that Alex Pott describes in the IS, for years. It's bad for core DX.

wim leers’s picture

Quoting https://httpd.apache.org/docs/current/mod/mod_headers.html#Header:

set
The response header is set, replacing any previous header with this name. The value may be a format string.

(emphasis mine)

This means this is simply a bug in Apache. We're not the only one to notice/experience this: https://serverfault.com/questions/837782/apache2-header-always-set-merge...

I tried changing it to

  Header always unset X-Content-Type-Options
  Header always set X-Content-Type-Options nosniff

But that sadly doesn't seem to work either. It seems that in case of 4xx responses, mod_headers seems to somehow fail to inspect response headers, and hence ends up simply appending?!

The only thing I could think of that could still work, is something like:

  Header always set X-Content-Type-Options nosniff "expr=%{SCRIPT_NAME} != 'index.php'"

i.e. only do this if the script name (i.e. after executing RewriteRule ^ index.php [L]) is NOT index.php, so that we only do this for static files, since in all other cases, \Drupal\Core\EventSubscriber\FinishResponseSubscriber will already set this header for us anyway!

Sadly, after more than an hour of debugging, I still can't get it to work. Debugging Apache is a painful mess.

Hopefully this helps the next person who looks at this, and hopefully they can bring it to completion…

In case it helps, here are my many sorry attempts to either make that idea of mine work, or to at least debug it:

#  Header always unset X-Content-Type-Options "expr=%{REQUEST_STATUS} == 403"
#    Header always set X-Content-Type-Options nosniff "expr=%{LA-U:REQUEST_FILENAME} == index.php"

#Header always set DBG %{SCRIPT_FILENAME}
#    Header always set YAR HAR "expr=%{SCRIPT_NAME} == 'index.php'"
#    Header always set YAR HAR "expr=%{SCRIPT_NAME} && %{SCRIPT_NAME} != 'index.php'"
Header always set YAR HAR "expr=!%{SCRIPT_NAME}"

   Header always set X-Content-Type-Options nosniff "expr=%{REQUEST_URI} != 'index.php'"

#  Header always set X-Content-Type-Options nosniff "expr=%{REQUEST_URI} =~ m#^index\.php$#"
#  Header always set X-Content-Type-Options nosniff "expr=%{REQUEST_URI} !~ m#^index\.php$#"

FWIW, https://httpd.apache.org/docs/current/expr.html is mostly useless. :(

gwagroves’s picture

Also had no luck with mod_headers, but as a workaround, I left the .htaccess as is and implemented an event subscriber to strip out the header added in PHP:

<?php

namespace Drupal\my_module\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
 * Response subscriber to handle finished responses.
 */
class FinishResponseSubscriber implements EventSubscriberInterface {

  /**
   * Sets extra headers on successful responses.
   *
   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
   *   The event to process.
   */
  public function onRespond(FilterResponseEvent $event) {
    if (!$event->isMasterRequest()) {
      return;
    }

    $event->getResponse()->headers->remove('X-Content-Type-Options');
  }

  /**
   * Registers the methods in this class that should be listeners.
   *
   * @return array
   *   An array of event listener definitions.
   */
  public static function getSubscribedEvents() {
    $events[KernelEvents::RESPONSE][] = ['onRespond'];
    return $events;
  }

}

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

mr.baileys’s picture

Note sure if this still is an issue. I tried to reproduce it with Apache 2.4.25, but when fetching the homepage of a 8.9.x instance, the X-Content-Type-Options: nosniff only appears once:

curl -I localhost:32801
HTTP/1.1 200 OK
Date: Thu, 31 Oct 2019 10:27:53 GMT
Server: Apache/2.4.25 (Debian)
Cache-Control: must-revalidate, no-cache, private
X-Drupal-Dynamic-Cache: MISS
X-UA-Compatible: IE=edge
Content-language: en
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Expires: Sun, 19 Nov 1978 05:00:00 GMT
Vary:
X-Generator: Drupal 8 (https://www.drupal.org)
X-Drupal-Cache: MISS
Content-Type: text/html; charset=UTF-8



Note: the empty Vary-header is be covered in #3089143: FinishResponseSubscriber adds empty Vary-headers on non-cacheable responses.

alex-b’s picture

Apache 2.4.7 added a setifempty action in the headers module. The following eliminates the problem for me on D7:

<IfModule mod_headers.c>
  # Disable content sniffing, since it's an attack vector.
  Header setifempty X-Content-Type-Options nosniff
</IfModule>

Not sure how to turn this into a general solution, but may be a workaround for some.

alex-b’s picture

StatusFileSize
new456 bytes

According to the Apache mod_headers documentation, the onsuccess (default if ommitted) and always conditions process separate internal tables of HTTP headers. And indeed using

Header unset X-Content-Type-Options
Header always set X-Content-Type-Options nosniff

also fixes the problem for me. Note the absence of always in the first line compared to comment #12 above. Patch against 7.x-dev enclosed.

Edit: Also, having always in both of these lines still does trigger the header duplication.

liam morland’s picture

The solution in #19 is working for me in D8.8.1.

Is there a reason #20 is preferable to #19?

alex-b’s picture

Yes, the reason is that it should work with Apache versions older than 2.4.7 (where setifempty was introduced), as early as 2.0.

liam morland’s picture

Patch for Drupal 8.9.x. It probably applies to 9.0.x. as well.

Status: Needs review » Needs work
liam morland’s picture

Status: Needs work » Needs review
StatusFileSize
new1016 bytes

#23 with scaffold file also updated.

mr.baileys’s picture

Status: Needs review » Needs work

I was able to reproduce the issue after manually compiling Apache 2.4.6+PHP 7.2, both visually (duplicated headers) as well as confirming the failing RouterTest::testFinishResponseSubscriber:

curl -I http://d8.ddev.site:8080/
HTTP/1.1 200 OK
Date: Sun, 16 Feb 2020 15:28:27 GMT
Server: Apache/2.4.6 (Unix) PHP/7.2.27
X-Content-Type-Options: nosniff
X-Powered-By: PHP/7.2.27
Cache-Control: must-revalidate, no-cache, private
X-Drupal-Dynamic-Cache: MISS
X-UA-Compatible: IE=edge
Content-language: en
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Expires: Sun, 19 Nov 1978 05:00:00 GMT
Vary:
X-Generator: Drupal 8 (https://www.drupal.org)
X-Drupal-Cache: HIT
Content-Type: text/html; charset=UTF-8

I have also confirmed that the solution in #19 (using setifempty) will not work on Apache <2.4.7 (causes a 500 internal server error). Since Drupal does not specify an minimum Apache version other than 2.x, we unfortunately cannot use that solution (as explained in #22).

After applying the patch from #25, the "X-Content-Type-Options" header is present and not duplicated for A. static error pages (404, /does-not-exist.jpg), B. Dynamic error pages (404, /foo-bar), C. Regular pages (<front>) and D. static assets (/INSTALL.txt):

curl -I http://d8.ddev.site:8080/
HTTP/1.1 200 OK
Date: Sun, 16 Feb 2020 15:29:34 GMT
Server: Apache/2.4.6 (Unix) PHP/7.2.27
X-Content-Type-Options: nosniff
X-Powered-By: PHP/7.2.27
Cache-Control: must-revalidate, no-cache, private
X-Drupal-Dynamic-Cache: MISS
X-UA-Compatible: IE=edge
Content-language: en
X-Frame-Options: SAMEORIGIN
Expires: Sun, 19 Nov 1978 05:00:00 GMT
Vary:
X-Generator: Drupal 8 (https://www.drupal.org)
X-Drupal-Cache: HIT
Content-Type: text/html; charset=UTF-8

The suggested approach removes the X-Content-Type-Options header set by Drupal and replaces it with a new one, but since "nosniff" is the only allowed value, and since we always want this to be set, there is no risk of accidentally overwriting custom data by removing/resetting.
+++ b/.htaccess
@@ -182,6 +182,8 @@ AddEncoding gzip svgz
+  # Unset header to ensure no header duplication.
+  Header unset X-Content-Type-Options

I think this comment needs to be clearer, since it does not answer the question "why unset+set to avoid duplicate headers instead of not setting it at all". We should refer to the fact that X-Content-Type-Options is set by FinishResponseSubscriber, as without that context, this comment and the following directives are confusing.

Other than that, I think this is RTBC.

mr.baileys’s picture

Thinking some more about this, I think we should just up the minimum required Apache version for D8 to Apache >=2.4.7 (right now it is 2.x) and use the more elegant solution from #19. I doubt many people are still stuck on <= 2.4.6 (which is over 6 years old), and if they are, then they have a large number of vulnerabilities to worry about...

gisle’s picture

+1 on upping the minimum required Apache version. I also think this should be done for Drupal 7 as well (I already use the suggestion in #19 for Drupal 7).

mr.baileys’s picture

Opened #3114079: Update minimum supported Apache version to >= 2.4.7 to see if upping the minimum version for Apache is acceptable.

liam morland’s picture

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

Patch in #25 with updated comments attached.

I agree with upping the Apache version. Unless this happens quickly, I believe we should move ahead with the attached patch. We can always return and use setifempty later.

longwave’s picture

Can we use merge instead of set, i.e.

Header merge X-Content-Type-Options nosniff
merge
The response header is appended to any existing header of the same name, unless the value to be appended already appears in the header's comma-delimited list of values. When a new value is merged onto an existing header it is separated from the existing header with a comma. This is the HTTP standard way of giving a header multiple values. Values are compared in a case sensitive manner, and after all format specifiers have been processed. Values in double quotes are considered different from otherwise identical unquoted values.

As nosniff is the only valid value, this should add it if it's not already present, and ignore it if it is?

Although the docs don't explicitly say, in a quick test it does seem to add it if the header doesn't exist at all.

edit: #462950-44: Mitigate the security risks that come from IE, Chrome and other browsers trying to sniff the mime type mentions the same idea

longwave’s picture

StatusFileSize
new1010 bytes
liam morland’s picture

I think always should still be there.

mr.baileys’s picture

Status: Needs review » Needs work

@Liam Morland is correct in #33, and RouterTest::onFinishResponseSubscriber() is still failing locally on Apache 2.4.6 with the patch from #32.

From the Apache docs:

The optional condition argument determines which internal table of responses headers this directive will operate against: onsuccess (default, can be omitted) or always. The difference between the two lists is that the headers contained in the latter are added to the response even on error, and persisted across internal redirects (for example, ErrorDocument handlers). Note also that repeating this directive with both conditions makes sense in some scenarios because always is not a superset of onsuccess with respect to existing headers:

Adding always to the changes in #32 makes RouterTest::onFinishResponseSubscriber() pass locally.

mr.baileys’s picture

Although the docs don't explicitly say, in a quick test it does seem to add it if the header doesn't exist at all.

I can confirm that this is indeed the behaviour for merge even on an older Apache version like 2.4.6.

longwave’s picture

Status: Needs work » Needs review
StatusFileSize
new1 KB
mr.baileys’s picture

Status: Needs review » Needs work

While using merge makes the RouterTest pass locally, applying the patch from #36 still returns duplicate headers when testing manually:

curl -I http://d8.ddev.site:8080/
HTTP/1.1 200 OK
Date: Wed, 19 Feb 2020 09:36:31 GMT
Server: Apache/2.4.6 (Unix) PHP/7.2.27
X-Content-Type-Options: nosniff
X-Powered-By: PHP/7.2.27
Cache-Control: must-revalidate, no-cache, private
X-Drupal-Dynamic-Cache: MISS
X-UA-Compatible: IE=edge
Content-language: en
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Expires: Sun, 19 Nov 1978 05:00:00 GMT
Vary:
X-Generator: Drupal 8 (https://www.drupal.org)
X-Drupal-Cache: HIT
Content-Type: text/html; charset=UTF-8


This is not the case when I apply the patch from #30

liam morland’s picture

Status: Needs work » Needs review

I think the problem is that set and merge both don't notice that the header is already set, perhaps because it was set in PHP and not Apache. This strikes me as an Apache bug, but that shouldn't stop us from working around it here. Fortunately, unset works.

I'm proposing we stick with #30.

mr.baileys’s picture

Status: Needs review » Reviewed & tested by the community

Since it is unlikely we can increase the minimum Apache version to >2.4.6 (see #3114079: Update minimum supported Apache version to >= 2.4.7) which would allow us to use Header setifempty, and since using Header merge does not (always) seem to prevent the duplicate headers, I agree with @Liam Morland in #38.

The patch in #30 ensures that RouterTest passes locally and resolves the duplicate header issue, so RTBC.

liam morland’s picture

Patch #30 also applies to 9.0.x.

liam morland’s picture

longwave’s picture

+++ b/.htaccess
@@ -150,6 +150,9 @@ RewriteBase /~lkmorlan/drupal-7
+  # This header is also set in FinishResponseSubscriber. Unset before adding to

Drupal 7 doesn't have a FinishResponseSubscriber. This looks like it should be drupal_page_header() instead.

liam morland’s picture

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

It's quite confusing what the actual rtbc patch on this issue is. I think it is #30 is the rtbc patch. Can this be the last patch on the issue so the rtbc re-test gets it right an it's obvious what to do.

Re D7 patches - that needs a separate issue as we no longer solve them as part of the same issue.

liam morland’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new1.14 KB

Patch from #30. Restoring status.

Child issue created for D7 port.

liam morland’s picture

In #3114079: Update minimum supported Apache version to >= 2.4.7, it was decided that as of Drupal 9, Apache 2.4.7 would be required. Attached is a patch for D9 that uses merge.

liam morland’s picture

mr.baileys’s picture

Status: Reviewed & tested by the community » Needs work

I ran some manual tests with both Apache 2.4.7 and Apache 2.4.41, here are my findings (✗ = duplicate X-Content-Type-Options header, ✓ = single X-Content-Type-Options header):

    Dynamic Dynamic 404 Static Static 404 (handled by D8)
Apache 2.4.7 Merge  ✗  ✗
  Unset/set
Apache 2.4.41 Merge  ✗  ✗  ✗
  Unset/set

Conclusion: using merge does not fix the issue (see also comment #37 where similar manual testing yielded the same result.), using the unset/set technique does address the issue on all supported versions of Apache.

mr.baileys’s picture

As per #39, we upgraded the minimum Apache version so we would be able to use setifempty rather than merge. I have not tested this variant in #48, but some very quick testing indicates that this does resolve the issue.

liam morland’s picture

Version: 8.9.x-dev » 9.0.x-dev
Status: Needs work » Needs review
StatusFileSize
new1.01 KB

Sorry about that. Patch using setifempty for 9.0.x.

For 8.9.x, the patch in #45 is current and had acheived RTBC.

thalles’s picture

On my laptop show me this (D8.9.x php7.2):

D9.x php7.3:

mr.baileys’s picture

  • I am unable to reproduce the issue on Apache 2.4.38 (with or without the patch, there is no header duplication).
  • When I test the most recent patch against Apache 2.4.7, I'm still seeing duplicate headers unfortunately.

@thalles: just to confirm, were your screenshots taken after applying the patch in #50?

liam morland’s picture

It may be that newer Apache has fixed a bug. Header set should not have lead to a duplicated header anyway since it is supposed to set the value of the header, but in our case it has been behaving like add.

Are you saying that in Apache 2.4.7, the patch in #50 (setifempty) does not fix the problem but that the patch in #45 (unset & set) does?

thalles’s picture

I didn't apply any patches

mr.baileys’s picture

It may be that newer Apache has fixed a bug.

The testing in #18 and 52 indicates that the problem does not occur with Apache 2.4.25 & 2.4.38. The headers in the original report were generated using Apache 2.4.23, so that would mean the behavior changed in Apache 2.4.25 (2.4.24 was never released). I will run some tests later today to confirm this.

Are you saying that in Apache 2.4.7, the patch in #50 (setifempty) does not fix the problem but that the patch in #45 (unset & set) does?

Correct.

mr.baileys’s picture

Ignore my previous comment, I *am* able to reproduce the duplicate headers, even with the most recent version of Apache. I am not sure why the header duplication was not occuring during my previous tests, or in @thalles' test.

Regardless of version, the only solution that works for me is unset/set.

thalles’s picture

What is your drupal version?

liam morland’s picture

Perhaps we should use unset and set for all versions.

liam morland’s picture

This is the patch from #45 which uses unset and set. It had reached RTBC. Given the above, I propose that we use this for all versions of D8.

liam morland’s picture

Status: Needs review » Reviewed & tested by the community

The patch in #59 is the patch from #30 which had reach RTBC in #39. Various tests reported above have found no better solution. Restoring status.

alexpott’s picture

Status: Reviewed & tested by the community » Needs review

Header merge X-Content-Type-Options nosniff works just fine for me on Apache/2.4.41 (Unix).

When testing this you need to test against a 200 generated by drupal, a 40x generated by Drupal and against a file not served by Drupal code. With that in my .htaccess I get

 ~/d/s/drupal8alt.dev   9.0.x      curl -I http://drupal8alt.test                                                                                                                                    15.1s  Sat  4 Apr 07:11:16 2020
HTTP/1.1 200 OK
Date: Sat, 04 Apr 2020 06:11:18 GMT
Server: Apache/2.4.41 (Unix) OpenSSL/1.0.2s PHP/7.3.13
X-Powered-By: PHP/7.3.13
Cache-Control: must-revalidate, no-cache, private
X-Drupal-Dynamic-Cache: MISS
X-UA-Compatible: IE=edge
Content-language: en
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Drupal-Cache-Tags: block_view config:block.block.stark_admin config:block.block.stark_branding config:block.block.stark_local_actions config:block.block.stark_local_tasks config:block.block.stark_messages config:block.block.stark_page_title config:block.block.stark_tools config:block_list config:system.menu.admin config:system.menu.tools config:system.site config:user.role.anonymous config:user.settings http_response local_task rendered
X-Drupal-Cache-Contexts: languages:language_interface route theme url.path url.query_args user.permissions user.roles:authenticated
Expires: Sun, 19 Nov 1978 05:00:00 GMT
X-Generator: Drupal 9 (https://www.drupal.org)
X-Drupal-Cache: HIT
Content-Type: text/html; charset=UTF-8

 ~/d/s/drupal8alt.dev   9.0.x      curl -I http://drupal8alt.test/core/MAINTAINERS.txt                                                                                                                       Sat  4 Apr 07:11:18 2020
HTTP/1.1 200 OK
Date: Sat, 04 Apr 2020 06:11:21 GMT
Server: Apache/2.4.41 (Unix) OpenSSL/1.0.2s PHP/7.3.13
Last-Modified: Sat, 28 Mar 2020 05:51:30 GMT
ETag: "3bed-5a1e3cf3bbc80"
Accept-Ranges: bytes
Content-Length: 15341
Vary: Accept-Encoding
X-Content-Type-Options: nosniff
Content-Type: text/plain

 ~/d/s/drupal8alt.dev   9.0.x      curl -I http://drupal8alt.test/doesnotexist                                                                                                                               Sat  4 Apr 07:11:21 2020
HTTP/1.1 404 Not Found
Date: Sat, 04 Apr 2020 06:11:37 GMT
Server: Apache/2.4.41 (Unix) OpenSSL/1.0.2s PHP/7.3.13
X-Powered-By: PHP/7.3.13
Cache-Control: must-revalidate, no-cache, private
X-Drupal-Dynamic-Cache: UNCACHEABLE
X-UA-Compatible: IE=edge
Content-language: en
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Drupal-Cache-Tags: 4xx-response block_view config:block.block.stark_admin config:block.block.stark_branding config:block.block.stark_local_actions config:block.block.stark_local_tasks config:block.block.stark_messages config:block.block.stark_page_title config:block.block.stark_tools config:block_list config:system.menu.admin config:system.menu.tools config:system.site config:user.role.anonymous http_response local_task rendered
X-Drupal-Cache-Contexts: languages:language_interface route theme url.query_args:_wrapper_format user.permissions
Expires: Sun, 19 Nov 1978 05:00:00 GMT
X-Generator: Drupal 9 (https://www.drupal.org)
X-Drupal-Cache: MISS
Content-Type: text/html; charset=UTF-8

If I add the always to the .htaccess directive then it breaks but the always is not necessary. 40x and 50x are generated by Drupal and will have the finish response subscriber run so it's not adding any protection as far as I can see. setifempty also works as long as the always keyword is not there.

Before committing #59 I think we need to confirm that the always is necessary. I'm not sure it is.

xjm’s picture

Version: 9.0.x-dev » 9.1.x-dev
Issue tags: +Needs release note

This might be minor-only since it involves changes to .htaccess. It's also something we mention in release notes.

Since 8.9.x and 9.0.x are now in beta, I'm moving this to 9.1.x. Thanks!

liam morland’s picture

liam morland’s picture

This issue is about duplicate headers. The always is already there. Making a change to that should be a separate issue.

merge does something different. This header could be set to a different value, which means merge would leave us with both values, so set is what we want.

liam morland’s picture

Status: Needs review » Needs work
longwave’s picture

Status: Needs work » Needs review

Unrelated fail

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

liam morland’s picture

Created merge request with #65.

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

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

liam morland’s picture

Re-rolled for 9.3.x.

effulgentsia’s picture

Would be nice to have test coverage for this fix. I guess we already do per #4, except DrupalCI doesn't include mod_headers. I opened #3226187: Enable mod_headers and mod_expires on Apache for that.

It probably makes sense to commit this without waiting on that. I'm holding off for the moment, but if I or someone else doesn't commit this by Wednesday, please add a comment here then to remind me.

effulgentsia’s picture

effulgentsia’s picture

Since this MR is using the pattern that's recommended in https://httpd.apache.org/docs/current/mod/mod_headers.html (see "To circumvent this limitation..." on that page), I'm thinking it might help people who read this code later if we add an inline comment that links to that.

larowlan’s picture

Status: Reviewed & tested by the community » Needs work

Needs work for #76

effulgentsia’s picture

Status: Needs work » Needs review

I pushed a commit to the MR that addresses #76.

liam morland’s picture

I don't know why it created a second merge request. Use !323.

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.

liam morland’s picture

Re-rolled for 9.4.x.

longwave’s picture

Status: Needs review » Reviewed & tested by the community

#76 was addressed, so back to RTBC.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs change record

As this is changing the root .htaccess file we need a change record and a release note in the issue summary see https://www.drupal.org/node/3186526 for an example CR and https://www.drupal.org/project/drupal/issues/3186524 has the release note in the issue summary.

Once these things exist the issue can be set back to rtbc.

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.

joshahubbers’s picture

StatusFileSize
new976 bytes

Sorry for the clutter but the diff above won't apply because the .htaccess is not present when you build the project with composer. That file will be copied from the scaffold folder. So this patch only patches the scaffold file.

joshahubbers’s picture

liam morland’s picture

liam morland’s picture

Status: Needs work » Needs review
longwave’s picture

Status: Needs review » Needs work

This still needs a change record and release note.

joshahubbers’s picture

Issue summary: View changes
joshahubbers’s picture

I added the release note to the summary, but I cannot add a change record. I will add a change record now.

joshahubbers’s picture

Status: Needs work » Needs review

I added a draft for the change record. I is my first, hope it is sufficient.

longwave’s picture

Issue summary: View changes
Issue tags: -Needs release note, -Needs change record

Thanks! I tweaked both the note and change record a little but they look good to me.

idebr made their first commit to this issue’s fork.

idebr’s picture

Status: Needs review » Reviewed & tested by the community

#85 Change record and Release note is available, setting back to RTBC

xjm’s picture

Asking from complete ignorance: Is there any web.config equivalent we should consider here for parity?

longwave’s picture

@xjm Great question. https://serverfault.com/a/904278 suggests it is both possible to do and to prevent duplicate headers, as in the .htaccess changes in this issue. However I think it would be necessary for someone running IIS to test this before we commit such a change.

xjm’s picture

Status: Reviewed & tested by the community » Needs work

Great, let's do that then. Thanks!

liam morland’s picture

Status: Needs work » Reviewed & tested by the community

The web.config file is completely different. This should be done in a follow-up ticket.

alexpott’s picture

Also web.config does not currently do anything with X-Content-Type-Options so it can't be causing the same bug. This fact might be a bug on it's own but it shouldn't be solved here as this issue is about Apache having duplicate headers.

alexpott’s picture

Also a decent number of IIS articles recommend setting this on the site level in the server (i.e. not using web.config - see https://dotnetcoretutorials.com/2017/01/20/set-x-content-type-options-as... for an example).

longwave’s picture

xjm’s picture

If IIS doesn't have it set at all yet, then @catch, @longwave, and I agreed with a followup.

However, please make note that in general, any issues for .htaccess should consider web.config in the same issue scope. In this case, probably the previous issue should have. We've had frequent issues over the years (both public hardenings and private security issues) where the web.config has not been well-maintained and has not gotten the same fixes, so we always need to evaluate it @Liam Morland. It doesn't get excluded just because it's in a different file, and how issues are scoped is a release management decision.

  • xjm committed e7b87b5c on 10.1.x
    Issue #2854817 by Liam Morland, longwave, alexpott, JoshaHubbers,...
xjm’s picture

Issue summary: View changes
Status: Reviewed & tested by the community » Fixed
Issue tags: +10.1.0 release notes
  # https://httpd.apache.org/docs/current/mod/mod_headers.html.
  Header onsuccess unset X-Content-Type-Options
  Header always set X-Content-Type-Options nosniff

Since we're unsetting all values for the header but only resetting nosniff, I checked to verify that this was the only possible value for it. Per https://fetch.spec.whatwg.org/#x-content-type-options-header:

  1. Let values be the result of getting, decoding, and splitting `X-Content-Type-Options` from list.
  2. If values is null, then return false.
  3. If values[0] is an ASCII case-insensitive match for "nosniff", then return true.
  4. Return false.

So at least in the present spec, this solution is OK.

Adding credit for @alexpott for code review and the initial issue report, @Liam Morland, @alex-b, and @Mile23 for work on the patch, @longwave for review and work on the CR and release note, @JoshaHubbers for work on the CR and release note, @effulgentsia for review and work on the patch, @Wim Leers for review and issue triage, and @mr.baileys for review and manual testing. I did not credit @idebr for merging HEAD with no other changes.

I was wondering what happened to the earlier attempts at test coverage, but it seems #3226187: Enable mod_headers and mod_expires on Apache needs to be addressed for that.

Release notes should always tell the user:

  1. What it was before
  2. What changed
  3. Who might be affected
  4. What they should do as a result

The release note was missing the second two, so I added a sentence for that. I similarly expanded the content of the CR with background information on why this header is used in the first place, and put in a code snippet to show exactly what site owners should update. Finally, I linked the CR from the release note.

Committed and pushed to 10.1.x. Thanks!

xjm’s picture

Added the release note to the draft release notes doc.

greggles’s picture

I appreciate all the work on this issue. Thanks to everyone involved.

However, please make note that in general, any issues for .htaccess should consider web.config in the same issue scope. In this case, probably the previous issue should have. We've had frequent issues over the years (both public hardenings and private security issues) where the web.config has not been well-maintained and has not gotten the same fixes, so we always need to evaluate it @Liam Morland. It doesn't get excluded just because it's in a different file, and how issues are scoped is a release management decision.

From the security team perspective, I support this paragraph from comment #106. We regularly will get reports "htaccess provides X security benefit that is missing in web.config" and then are in the awkward situation of deciding whether to do a security advisory and release for a fix that affects relatively few people.

I think we should consider it a bug if web.config and htaccess' behavior is different.

xjm’s picture

Published the CR.

idebr’s picture

Is this commit eligible for backporting as it is a bugfix?

longwave’s picture

Changes to .htaccess are usually only made in minor releases unless they are critical. This is not a critical bug - the duplicate header might cause warnings, but it has no actual impact on anything - so to me this is not eligible for backport.

Status: Fixed » Closed (fixed)

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