Drupal 8 and content security policies

A Content Security Policy is a HTTP response header that helps reduce XSS risks by declaring which resources can be included by your wesbite.

First we should explore what a content security policy (CSP) actually is and how it can benefit your site. A CSP is a directives based whitelist that is delivered when your page is requested. These directives instruct the browser how it should handle including resources from alien domains.

The CSP recommendation has a number of resources that you can use to help harden your website. Some of the common ones that are implemented:

  • script-src
  • style-src
  • img-src
  • font-src

These directives should contain a whitelist of domains that you want to allow for your website. It is important to remember that if you haven’t defined a particular directive in your CSP the default browser implementation will allow all domains.

Let’s say we need to include Google Analytic tracking, this requires us to trust a third-party source. If we don’t define a CSP the third-party is implicitly trusted, however this means that the third-party could also include scripts from another place. You may never actually know what scripts are being included nor will you know exactly what they’re doing.

To ensure that only the script that you’re expecting is included you should add directives to your CSP definition which could look like:

Content-Security-Policy: script-src 'self' www.google-analytics.com;img-src www.google-analytics.com;

This directive would ensure that:

  • Scripts can be executed from your domain (using the self keyword)
  • Scripts can be included from http or https www.google-analytics.com
  • Images can be included from www.google-analytics.com

Anything that falls outside of these allowed domains would be blocked by the browser and not be allowed to execute on your page. Conversely the missing directives from your CSP would have the browser default effectively allowing everything.

Report-only

The CSP specification provides an additional header that allows the browser to report on CSP violations as opposed to directly blocking them. This can be useful when attempting to create a robust CSP and is a useful method for testing changes to a CSP before deploying them to your site. A report only mode CSP is added with the Content-Security-Policy-Report-Only header.

Content-Security-Policy-Report-Only: <policy-directive>; <policy-directive>

You should be aware that when implementing a report-only CSP, it is a potential vector for a denial of service attack. When the browser triggers a CSP violation it initiates a request to the configured domain. Unlike traditional requests the browser does not wait for a response from the domain and as a result can initiate a vast number of requests in quick succession.

Using HTML tags

I spoke briefly about using a meta tag in a previous post lets explore this option a little more as well. This is set using the HTML meta tag http-equiv="Content-Security-Policy" with the content attribute specifying the directives listed above. It is important to note that a CSP delivered via the meta tag will be merged with a CSP header before being enforced.

<meta http-equiv="Content-Security-Policy" content="">

There are some other limitations of the meta tag however, it cannot define a report only CSP and cannot be used to whitelist frame ancestors.

Notes

An interesting thing to note when using the meta tag is that this needs to be included in your document before any resources are requested. There has been some discussion about this and it appears to be by design as the spec suggests:

Authors are strongly encouraged to place meta elements as early in the document as possible, because policies in meta elements are not applied to content which preceeds them.

For example:

<head>
  <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
  <meta
    http-equiv="Content-Security-Policy"
    content="default-src 'self'; script-src 'self' google-analytics.com">

In the above example the browser will include jQuery from the remote domain even though a CSP directive for script-src doesn’t define the domain as an allowed source. This seems to be intended behaviour

Browser support

There are currently two specifications for CSP implementations and both have varying degrees of browser support. What we have discussed so far mostly relates to CSP 1.0 which has the most support.

Content security policy 1.0

IE Edge Firefox Chrome Safari
11 14 51 56 10

Content security policy Level 2

IE Edge Firefox Chrome Safari
NA 15 51 (partial) 56 10

Changes between 1.0 and level 2

CSP is an evolving specification of the web and version 2.0 was released on December 15th 2016. The new version introduces a number of new directives but also has the drawback of being incompatible with 1.0. These incompatibilities have slowed the adoption of CSP2.0 by browser vendors.

The main incompatibility lies in script-src directives being moved to child-src when controlling nested browser contexts.

For a detail overview see the article from the W3C.

Implementing in Drupal

There are a number of ways that you could implement a CSP in Drupal- you could maintain your own custom module with hard coded explicit CSP lists; you could use a module – such as security kit – to manage the CSP via the UI; you could even manage configuration files for the security kit module.

I would recommend that you avoid using the HTML meta tag when implementing a CSP in Drupal as you should always have access to add headers.

Security Kit (seckit)

The security kit module provides allows managing of various security-hardening options via the UI. One of the options that it exposes is the ability to control the CSP.

Seckit UI

This method is the most flexible and allows you to manage your CSP dynamically with a configuration UI. It allows you to define all the directives listed by the spec and even allows you to define report-only mode mentioned above.

Custom module

Drupal 8 being built on the Symfony framework allows us to define services which can respond to events that Symfony (and Drupal for that matter) define. There are a number of Kernel events and are defined as constants on the Kernel object, they look like:

<?php
KernelEvents::REQUEST;
KernelEvents::RESPONSE;

For a full list check out Drupal.org.

To subscribe to an event you will need to ensure that you have a services.yml in your custom module.

services:
  example_subscriber:
    class: '\Drupal\example_subscriber\EventSubscriber\ResponseSubscriber'
    tags:
      - { name: 'event_subscriber' }

This class will be responsible for telling Drupal what method to call, what event to subscribe to and it should always implement EventSubscriberInterface. We need to define getSubscribedEvents, as defined by the interface, which will tell Drupal which events we want to hook into.

<?php

namespace \Drupal\example_subscriber\EventSubscriber;

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

class ResponseSubscriber implements EventSubscriberInterface {
  public static function getSubscribedEvents() {
    $events[KernelEvents::RESPONSE][] = ['onResponse'];
    return $events;
  }
}

Here we are telling Drupal that when it emits the KernelEvents::RESPONSE event that we want to call the onResponse method of this subscriber. This is where we will define the CSP definitions and add the header to the response object.

<?php

use Symfony\Component\HttpKernel\Event\FilterResponseEvent;

class ResponseSubscriber implements EventSubscriberInterface {

  public function onResponse(FilterResponseEvent $event) {
    $csp_definition = '';
    $response = $event->getResponse();
    $response->headers->set('Content-Security-Policy', $csp_definition);
  }
}

The methods will have the an instance of the FilterResponseEvent object given which allows us to access the response object.

Theme layer

Drupal 8 plans to allow you to define services in a theme. This will give you access to the event subscriber system which needs to be used to add HTTP headers to a response.

At the time of writing this is planned for Drupal 8.4 but the patch still needs work. For now; you’ll have to go with a custom module outlined above- soon though!

But which one?

I think it would be best to go with Seckit it provides a lot more security best practises than just CSP. The main downside with Seckit at the moment it is in an alpha release state with a performance issue blocking the release. If your project doesn’t allow Alpha modules; then a custom module is the only way to provide a CSP at this stage.

Further reading

Drupal 8 CSP