Setting up a Content Security Policy with Gatsby

November 14, 20195 min read

This is a quick story telling how I set up a content security policy on my personal blog which is powered by Gatsby. I’ve been keen to try adding CSP for a while but I know it can be tricky to get right and cover everything. However, this blog is a very simple use case as I embed very little third party scripts but it took me some time to get it right nevertheless.

First attempt

Gatsby is relatively young but has already a rich plugin ecosystem. So the first thing I did was to research any plugin that would help with this. I quickly found gatsby-plugin-csp doing exactly what I wanted. I went ahead and installed it without any options to give it a try:

// gatsby-config.js
module.exports = {
  plugins: [`gatsby-plugin-csp`],
}

I opened a pull request to check a production-like deploy preview on Netlify, but it looked pretty broken:

First try

Looks like a few things were missing… Let’s customise this plugin!

Starting to customise

The plugin has a few options, I quickly glanced the documentation and immediately started adding directives for the errors that were reported. I’m using Google analytics and Google fonts, I need to whitelist them:

// gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-csp`,
      options: {
        directives: {
          'script-src': `'self' www.google-analytics.com`,
          'style-src': `'self' 'unsafe-inline' fonts.googleapis.com`,
          'img-src': `'self' data: www.google-analytics.com`,
        },
      },
    },
  ],
}

Pushed to my branch again, waited for the deployment to be updated, but it was still not looking great:

With Options

Hum, weird it looks even worse! Ok the errors are different, let’s keep customising. Looking at the errors, I can see another Google font domain and inlines are missing, let’s add them:

// gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-csp`,
      options: {
        directives: {
          'script-src': `'self' 'unsafe-inline' data: www.google-analytics.com`,
          'style-src': `'self' 'unsafe-inline' fonts.googleapis.com fonts.gstatic.com`,
          'img-src': `'self' data: www.google-analytics.com`,
        },
      },
    },
  ],
}

Let’s deploy again… But now I’m confused:

Inline disallowed

It says some inlines are disallowed, but they are in my directive!

Time to read the documentation

After going back to the plugin documentation, I can see they have 2 options mergeScriptHashes and mergeStyleHashes, but I didn’t really understand what they are for. At the bottom of the plugin’s readme, there is a link to a blog post that I should have read first.

What happens? The plugin tries its best to generate a list of the hashes for the allowed inlines script and styles that Gatsby adds. It turns out that the CSP specification states that a policy cannot use both hashes and 'unsafe-inline', and to be fair, that’s what my browser is telling me in the first warning at the top of my console:

Content Security Policy: Ignoring "'unsafe-inline'" within script-src or style-src: nonce-source or hash-source specified

Lesson learned: read the warning/errors from top to bottom, not the other way around.

Solution

These 2 options were exactly what I needed, we can’t use unsafe-inline with hashes which are included by the plugin, we need to disable them.

// gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-csp`,
      options: {
        mergeScriptHashes: false,
        mergeStyleHashes: false,
        directives: {
          'script-src': `'self' 'unsafe-inline' data: www.google-analytics.com`,
          'style-src': `'self' 'unsafe-inline' fonts.googleapis.com fonts.gstatic.com`,
          'img-src': `'self' data: www.google-analytics.com`,
          'font-src': `'self' data: fonts.gstatic.com`,
        },
      },
    },
  ],
}

At this point, it all look good visually, but when checking security headers, I still had a B grade:

B Grade

Moving policy to the HTTP header

The plugin implements CSP by using a <meta> tag, not a HTTP header. They have an issue open to support HTTP headers, but it looks like it won’t be supported soon as it depends on the deployment platform used. The issue links to a comment with a solution to inject the CSP to the headers with the gatsby-plugin-netlify package. The patch is pretty self-contained so I decided to replicate it on this blog. Seem to work fine and after deploying, I finally got this A grade I was after:

A Grade

Success! Press merge and got it deployed.

Final words

Thanks for the very helpful resources out there, Thom Krupa & the Bejamas organisation as well as Sebastien Dubois for the script to make the header work.

The A grade is nice, however the policy I’m ending up with is not very secure and allows a lot of thing to be executed. Each time we need to resort to unsafe-inline, we’re opening up a breach for attackers. I don’t have much sensitive things on this site though, so it’s probably good enough for this case and this seems to be the best we can do for now. I’ll keep a close eye to the improvements in Gatsby and the CSP to see things evolve.