As of Drupal 7.40 the default htaccess file has changed (#462950: Mitigate the security risks that come from IE, Chrome and other browsers trying to sniff the mime type).
However, when apache does not have mod_headers enabled (or another web server is used), anonymous page caching is *enabled*, and the page is served from cache, the x-content-type-options nosniff is not added to the HTTP response.
It seems the issue is within drupal_serve_page_from_cache, as the line below does not contain "x-content-type-options":
if (in_array($name_lower, array('content-location', 'expires', 'cache-control', 'vary')) && !isset($hook_boot_headers[$name_lower])) {
drupal_add_http_header($name, $value);
unset($cache->data['headers'][$name]);
}
Steps to reproduce:
1) Disable apache mod_headers
2) Enable anonymous page caching
3) Browse to any node ("X-Drupal-Cache" is "MISS" and "X-Content-Type-Options nosniff" exists - it comes from drupal_page_header)
4) Browse again to same node ("X-Drupal-Cache" is "HIT" and "X-Content-Type-Options nosniff" does NOT exist - it comes from drupal_serve_page_from_cache)
Comments
Comment #2
liannario commentedComment #3
sumeetjaggi commentedHi,
Anyone could please review the attached patch I believe It should resolve the missing "x-content-type-options" and append it to header, when the Drupal default cache is enabled.
Thanks,
Sumeet
Comment #4
sumeetjaggi commentedComment #6
sumeetjaggi commentedThis patch passed https://www.drupal.org/pift-ci-job/507915 . Thanks!
Comment #8
sumeetjaggi commentedComment #9
kingdutch-- Attached patch should not be used, please read on | Marked as needs review because I'm curious what the testbot says about this --
Although the patch applies cleanly it doesn't seem to fix the issue. In my scenario I'm running on nginx and unable to set the header outside of Drupal.
I can't figure out what the correct solution would be exactly but I believe your fix is only partial. Your fix does allow the header to be cached so it's output on later page visits. However the header itself never makes it into the cache so it isn't actually sent to the browser once the page is loaded from cache.
The
drupal_page_header()function callsdrupal_send_headers()which directly sends the headers. When the page gets stored in the cache though the headers are loaded usingdrupal_get_http_header()which loads it from the static variable nameddrupal_http_headers. This static variable is an array that's only populated through the use ofdrupal_add_http_header().The quickest solution would be to call
drupal_add_http_header()instead ofdrupal_send_headers()fromdrupal_page_header()as the former will call the latter.A core maintainer may have to weigh in as to what the side effects of this are though because I'm not entirely familiar with the system.
I have a patch attached and hidden that implements the above train of thought. The reason I've hidden it is that it has the side effect of caching all 3 default headers. This causes the cache-control to be stuck at forcing the browser to revalidate at every turn.
A better solution is to call `drupal_add_http_header()` for `X-Content-Type_Options` explicitly but I'm unsure where that should happen.
Comment #10
kingdutchExplaining the problem in an issue queue post sometimes helps you figure things out.
The attached patch addresses the above issue. The above issue is caused by default_headers passed to
drupal_send_headersnot being able to overwrite headers explicitly set bydrupal_add_http_header. Rightfully so. I think the nosniff header is important enough to be opt-out and thus I have promoted it to a header that's always set indrupal_page_headerby callingdrupal_add_http_headeronly for that specific header.A user can disable the header by setting a different value when calling
drupal_add_http_headerelsewhere in the codebase. This along with the fix from SumeetJaggi causes the header to be properly sent for anonymous cached pages.An alternative to the attached patch of promoting the X-Content-Type-Options could be to add it as another default header before line 1574 in
drupal_serve_page_from_cachein bootstrap.inc (which would still require SumeetJAggi's addition).Comment #12
kingdutchAfter putting this down last night I have come to an additional insight. I can't exactly figure out how browsers cache headers but after reading RFC 2616, section 10.3.5 as mentioned on line 1549 of bootstrap.inc I have come to the realisation that the X-Content-Type-Options is probably cached as part of the page (in the way the browser treats the page the first time and thus upon every next request).
With this in mind I think it is correct that the "X-Content-Type-Options" header is not sent for requests that can be served from the browser cache. These terminate in a
304 Not Modifiedresponse at line 1577 of bootstrap.inc.The remaining issue is then that if a request is not served from browser cache (not terminating at line 1579 of bootstrap.inc) but still served from Drupal cache the X-Content-Types-Options is not sent. This is fixed when promoting the header to use
drupal_add_http_headerinstead of setting it as default header.Attached is a patch only implementing the first half of #10 where the header is set using
drupal_add_http_headerso it's correctly cached on Drupal's side and sent when serving pages from the Drupal cache. It omits the addition ofX-Content-Type-Optionson line 1552 so that the header is not resent when the page is to be served from the browser cache (as the browser has previously determined how to treat the resource).Comment #13
David_Rothstein commentedIn practice I'm not sure there's a need to set this header on cached page requests (does Drupal ever serve a user-uploaded file via the page cache?) but for consistency it definitely makes sense.
(Apparently I actually noticed this problem when committing the original patch, but I don't think I understood exactly why it happened :)
The explanation in this issue and the latest patch make sense on a first glance.... Do we know if this issue affects Drupal 8 also?
Comment #14
cafuego commentedThis is not an issue on Drupal 9, and it's fair enough that it shouldn't affect cached pages, but it seems to make commercial penetration testing providers generate spurious complaints about Drupal 7 sites (don't ask) so a fix would be nice :-)
I've refactored the patch so the header is set to a default value in drupal_serve_page_from_cache() (just as it is in drupal_page_header() and can be overridden as needed via hook_boot() so now you have two fixes to choose from. (sorry! :-)
Comment #15
mcdruid commentedAdded this issue to #3216978: [meta] Priorities for 2021-12-01 release of Drupal 7 for tracking, but that doesn't guarantee if/when it'll be reviewed and committed.
Comment #16
diego_mow commentedTested patch #14 just right now and it worked for me.
Marking this issue as RTBC.
Comment #17
mcdruid commentedD9 adds this header in
\Drupal\Core\EventSubscriber\FinishResponseSubscriber::onRespond():https://git.drupalcode.org/project/drupal/-/blob/9.4.0-alpha1/core/lib/D...
Is there any reason why D7 shouldn't also emit this header in the same place as X-Frame-Options (and Permissions-Policy and others) e.g.:
https://git.drupalcode.org/project/drupal/-/blob/7.89/includes/common.in...
A quick manual test suggests that approach seems to work.
I would also like to see a test added for this ideally; perhaps just a couple of lines added to an existing test for a response from cache. However, if the testbot runs apache, am I right in thinking the header will always be added by .htaccess anyway?
Comment #18
mcdruid commentedWriting a test ended up being a bit convoluted, but this approach seems to work okay.
The test adds a callback to system_test.module that retrieves an object from cache_page and returns any headers stored with it.
This means we can verify that Drupal itself emitted the header, rather than apache via .htaccess
Passes locally with the patch adding the header in common.inc but let's see if the testbot agrees.
Comment #19
mcdruid commentedOoops - I've put
page_cachein a couple of places and that should becache_page.Shouldn't affect the test as it's just in comments etc.. will fix later if we keep this test.
Comment #22
mcdruid commentedLet's try those again.
Comment #23
mcdruid commentedOops and this time without hacky Xdebug hacks.
Comment #24
mcdruid commentedComment #28
mcdruid commentedComment #29
cafuego commentedVery naughty, setting your own patch to RTBC.
Comment #30
mcdruid commented:) yes, fair enough.
I'm not sure we're following #3093258: [policy, no patch] Proposed Drupal 7 contributor / maintainer workflow strictly, but the general idea is that the maintainers will review issues we've tagged as "Pending Drupal 7 commit". So the Framework Manager (@Fabianx) will review any patches I've worked on before they're committed. On that basis I sometimes bend the rules and mark patches I've worked on as RTBC before the final review.
More eyes are always welcome though, so if anyone else wants to review (and hopefully RTBC) this.. that'd be great. The next release is due in a few weeks (#3259739: [meta] Priorities for 2022-06-01 release of Drupal 7).
Comment #31
mcdruid commentedWe should consider whether this is worth a Change Record as it may change the headers output by some sites.
e.g. if a site was using a menu callback to serve something like JS or CSS without setting the content type appropriately, this could possibly break that functionality.
Comment #32
cafuego commentedSorry about that, I meant to test this straight after my whine and then things happened.
I've applied the patch from #23 to my site and it works as advertised, thank you :-)
Comment #33
fabianx commentedRTBC! - Looks good to me, approved. Thanks all!
Comment #35
mcdruid commentedAdded a CR.
Thank you everyone!