Skip to the content
Category: WEB DESIGN

Content security policy on Netlify

How I implemented a content security policy on a static website built with Jekyll, hosted on Netlify and loaded with several external embeds.

What is a content security policy?

From Mozilla Developer Network:

Content Security Policy (CSP) is an added layer of security that helps to detect and mitigate certain types of attacks, including Cross-Site Scripting (XSS) and data injection attacks. These attacks are used for everything from data theft, to site defacement, to malware distribution.

Although using a static web site usually means you have to worry less about security, the big picture is a bit more complicated. I embed many <iframe> elements due to necessity. Bandcamp, Soundcloud, Spotify and YouTube, to name them all. Some of these embeds are generating awful code and tens of third-party scripts.

I could have sandboxed the iframes, but I wanted a more tight way to protect the entire website. Enter content security policy (CSP).

Implementing CSP on Netlify

In the past, using WordPress, I had to work on the Apache configuration to configure a web server to return the Content-Security-Policy HTTP header. The <meta> element can be used to configure a policy. For example:

<meta http-equiv="Content-Security-Policy"
      content="default-src 'self'; img-src https://*; child-src 'none';">

After the switch to Jekyll and Netlify the process is significantly simplified. I just created a specific netlify.toml configuration file in my repository and added the instructions.

The tricky part was to find out all the external resources I’m pinging, including those I wasn’t fully aware of. For example, webmentions are implemented through a Javascript calling https://webmention.io, which pulls in all the mentions including avatar images. For this specific case, in the Content-Security-Policy section I had to allow:

img-src 'self' https://webmention.io https://*.amazonaws.com;

The main difficulty about the aforementioned audio-visual embeds was connected to their habit of injecting inline CSS, which I decided to disallow. The fix came from forcing their iframes to print specific classes based on my inclusion method in Jekyll, and move the third-party inline CSS to my SASS theme.

Example: Bandcamp

Bandcamp comes with a fixed dimension iframe, depending on a few options when choosing the embed code on their site. Once I’d decided to always use the small version of the album picture, the main difference is the object height. I have three cases:

  • Single song
  • 2-song EP
  • Album

I turned their fixed height in two specific embed classes:

<div class="iframe-bandcamp my-5 bc-single">
[...]
<div class="iframe-bandcamp my-5 bc-ep">
[...]
<div class="iframe-bandcamp my-5 bc-album">

Note that in Bandcamp, whenever I have more than one song, I always check “Show tracklist”:

Embed options in Bandcamp
Embed options in Bandcamp

After pasting the code in my site, I strip the inline style="border: 0; [...] part, since I have my cases already defined in CSS:

.iframe-bandcamp iframe {
  border: 0;
  min-width: 470px;
  width: 100%;
}
.iframe-bandcamp.bc-single iframe {
  height: 120px;
}
.iframe-bandcamp.bc-ep iframe {
  height: 220px;
}
.iframe-bandcamp.bc-album iframe {
  height: 400px;
}

The code

Here is my security policy, which grants an A+ on benchmarks. As you can see, i don’t allow CSS and Javascript, either external or inline, from external sources. I’m also blocking the website, through x-frame-options, from being embedded in an iframe.

[[headers]]
  for = "/*"
  [headers.values]
    X-Frame-Options = "deny"
    X-XSS-Protection = "1; mode=block"
    X-Content-Type-Options = "nosniff"
    Referrer-Policy = "no-referrer"
    Strict-Transport-Security = '''
      max-age=31536000;
      includeSubDomains;
      preload
    '''
    Content-Security-Policy = '''
      default-src 'self' https://webmention.io;
      style-src 'self';
      img-src 'self' https://webmention.io https://*.amazonaws.com;
      script-src 'self';
      frame-src https://www.youtube-nocookie.com/embed/ https://open.spotify.com/embed/ https://w.soundcloud.com/player/ https://bandcamp.com/EmbeddedPlayer/
    '''
    Permissions-Policy = '''
      accelerometer=(none),
      ambient-light-sensor=(none),
      autoplay=(none),
      camera=(none),
      encrypted-media=(none),
      fullscreen=(none),
      geolocation=(none),
      gyroscope=(none),
      magnetometer=(none),
      microphone=(none),
      midi=(none),
      payment=(none),
      picture-in-picture=(none),
      speaker=(none),
      usb=(none),
      vibrate=(none),
      vr=(none)
    '''

Read more: