HTTP headers for security with Drupal

Last week, I saw this tweet https://twitter.com/Stargayte/status/1260654141421961216 in my Twitter news feed about the site https://observatory.mozilla.org, which, among other things, allows you to scan sites to see how security-related HTTP headers are set.

I have of course scanned my site, rectified certain settings, modified my tools so that this is automatically there and here is a subject for an article to write !

Constat

Here's the initial result, score of 40/100 (ouch!), the tests not passing were :

  • Content Security Policy
  • HTTP Strict Transport Security
  • X-Content-Type-Options
  • X-XSS-Protection

Settings :

  • HTTPS and HTTP redirect -> HTTPS already present

HTTP Strict Transport Security and X-XSS-Protection

Added in the configuration of the web server, Apache in my case :

<IfModule mod_headers.c>
Header set X-XSS-Protection "1; mode=block"
Header always set Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"
</IfModule>

X-Content-Type-Options

In reality this header is already handled by Drupal, but the problem is that it is present 2 times and with the same value.

There is an " event subscriber " in the core that adds the header and the header is added a second time with the Apache configuration provided by Drupal.

I've opened a issue to report the problem, as searching for " X-Content-Type-Options " in the issues didn't turn up anything and in fact an other issue has already existed for several years.

If you want to solve the problem while waiting for the consensus, it's not complicated, just remove the header and put it back by one of the components in the call chain (Apache, Varnish or Nginx). After that, as the header is already present, I'm not in a hurry, I'll wait until the issue is resolved.

Content Security Policy

Let's get straight to it, the most complicated point (to keep it polite) to deal with and make a rule as generic as possible.

The rule will no doubt need to be finely tuned for each site.

The result obtained is as follows, it is the result of applying the recommendations of the Mozilla site, while allowing Drupal to function :

Content-Security-Policy: "base-uri https 'self'; connect-src https 'self'; default-src 'none'; font-src https: 'self'; frame-ancestors https 'self'; img-src https: data:; object-src 'none'; script-src https: 'unsafe-inline'; style-src https: 'unsafe-inline'"

Edit 23/05/2020: Attention : The previous configuration is wrong, it should be https: and not https. See the "Edit 23/05/2020" section.

And here are the details of each rule :

  • base-uri : to avoid loading external JS.
  • connect-src : to allow certain modules to continue working :
    • Contextual Links,
    • Quich Edit.
  • default-src :'none' to avoid unexpected behavior.
  • form-action : not implemented because otherwise I had a strange behaviour, forms were submitted (because for example configuration was saved) but the page did not reload and there was an error message in the browser console saying that the form had not been submitted. I don't know if this was from my development environment with Traefik in reverse-proxy.
  • Frame-ancestors : to control which site can open an iframe or embed on the site.
  • Img-src : data to allow Blazy to provide an image while lazy-loading loads the real image.
  • Object-src : none to block this kind of tag.
  • Script-src : unsafe-inline to allow some JS to work :
    • CKEditor,
    • Matomo.
  • style-src : unsafe-inline otherwise some styles are removed :
    • styles generated by Modernizr JS,
    • inline style from CKEditor.

Bonus : a bit of offense

Offuscation isn't my philosophy at all, but hey, if it prevents it from triggering an attack from passing bots due to exposing excess headers, might as well implement it...

I had already set it up when the site went online, but now I've decided to make it the default in my tools.

  • PHP :
    • expose_php = off
  • Apache :
    • ServerSignature Off
  • Varnish :
    • sub vcl_deliver {
      ...
      unset resp.http.cache-tags;
      ...
      }
      
  • Nginx (Traefik equivalent) :
    • more_clear_headers 'Server';
      more_clear_headers 'Via';
      server_tokens off;

Conclusion

If we count X-Content-Type-Options as resolved, it's going to be complicated to get a better score than 80/100 because of the Content Security Policy header.

A way to go further would be to have a dedicated domain name (or a dedicated server) for administrator access (, contributor or moderator) because Modernizr only concerns the administrator and as a general rule CKEditor too. So apart from Matomo, it would be possible to avoid having to put unsafe-inline on style-src and script-src.

For Matomo, I think that by reviewing how the module works and making it load a generated JS file as the GoogleTagManager module allows you to do, that should solve the problem.

After that, we'll have to look at more complex sites, from the moment there's going to be a need to integrate with an external service that works with inline JS, this is likely to become very costly if not impossible depending on project constraints (planning, cost).

Edit 23/05/2020

After learning about the Content-Security-Policy module, I decided to remove the addition of this header by Nginx (or Traefik) and use the Drupal module instead.

In effect, the module will allow fine-grained management of the header, something that a server configuration won't (or hardly) allow. What's more, since it has to be adapted to the site's needs, it might as well be the application that manages the header.

One advantage of the module, it allowed me to realise that in my previous configuration, I had made errors entering 'https', you have to enter 'https:'. This is checked by the module when the configuration is saved.

The https: correction highlighted that this was not a configuration considered secure because it was too permissive for base-uri, frame-ancestors and script-src. But it's OK for img-src. You really need a white list of domains to authorise. I left https: for img-src because I use it for example for the copyleft at the bottom of the page and images on my Badges page.

Finally, I made patches for the Blazy and Matomo modules, something I had put in a "To go further" in the original conclusion, but which I was obliged to do to use the CSP module serenely. The issue with the links to the modules concerned: https://www.drupal.org/project/csp/issues/3099548

For information, here is the configuration of the CSP module that I have at the time of writing:

 report-only:
  enable: false
  directives:
    script-src:
      base: self
    script-src-attr:
      base: self
    script-src-elem:
      base: self
    style-src:
      base: self
    style-src-attr:
      base: self
    style-src-elem:
      base: self
    frame-ancestors:
      base: self
  reporting:
    plugin: none
enforce:
  enable: true
  directives:
    default-src:
      base: none
    connect-src:
      base: self
    font-src:
      base: self
    img-src:
      sources:
        - 'https:'
      base: self
    object-src:
      base: none
    script-src:
      base: self
    style-src:
      base: self
    base-uri:
      base: self
    form-action:
      base: self
    frame-ancestors:
      base: self
  reporting:
    plugin: none

This configuration will not necessarily be updated in the article, and is only there to serve as an example to adapt to your needs. Because of the complexity of providing a default value, I won't be scripting such a value in my Drupal project template either.

I've also tested the Security Kit module, which goes beyond the Content-Security-Policy header, but although it's more widely used, it offers fewer options than the Content-Security-Policy module regarding the header we're interested in, and it doesn't offer an event for adapting the header automatically in community or specific modules.

Edit 07/06/2020

In order to allow session cookies to be "secure" on my development environment, I managed the reverse_proxy and reverse_proxy_addresses settings in https://gitlab.com/florenttorregrosa-drupal/docker-drupal-project.

I wanted to re-test the "form-action" that was causing problems before, it's now OK. Apparently even though locally Drupal generated links in HTTPS, it didn't consider the requests as "secure" and therefore acted as if it were in HTTP.

Acknowledgements

Thanks to Philippe Joulot (phjou) for his tweets about https://observatory.mozilla.org and the Content-Security-Policy module.

Thanks to Geoff Appleby (gapple), maintainer of the Content-Security-Policy module, for his help.

Links

="" ul="">

Resources :

Comments

Add new comment