Content Security Policy (CSP)

Ok so bit more of a gap between posts than I intended but in September 2021 we took a massive step and bought our first home! I also finally got my AWS Solutions Architect Associate Certification.

Enough excuses, I want to start my 2022 content creation push by looking at a really powerful but often tricky to implement layer of protection; Content Security Policy (CSP). We will take a look at what CSP is and why you should use it. I will also go over some of the issues of deploying it, focusing in this post on deploying it to a Netlify blog.

What is CSP

CSP allows developers to control what and how resources are loaded by the browser. Typically this is implemented using HTTP headers.

It is made up of one or more directives that allow for granular controls that cover the following areas

  • Ensuring scripts are safe and trusted
  • Enforcing resources are loaded from trusted sources
  • Some other miscellaneous controls like transparent HTTP to HTTPS upgrades amongst others.

CSP provides violation reports so you can be alerted to issues and also supports a report-only mode, which is vital when testing your policy directives in the wild. It can be very easy to break functionality with CSP!

Strict CSP

As I mentioned at the start CSP can be tricky to get right. A study by Google found that 99.34% of hosts with CSP use policies that offer no benefit against XSS, the same study provides some great suggestions on an approach that is called strict CSP.

Why use CSP

CSP forms a part of a defence-in-depth strategy. It can’t prevent your application from being vulnerable to XSS but it can often make it very difficult for those vulnerabilities to be exploited and give a higher chance of you being alerted to an issue before it is exploited.

Whilst CSP primarily helps to prevent cross-site scripting (XSS) attacks it also can help prevent Clickjacking attacks, enforce secure protocols and mitigate Magecart-style attacks.

Implementing CSP

The first thing I did was set up reporting. As mentioned you can configure your CSP to report back any violations to an endpoint of your choosing. You can build a system yourself to manage these reports or do what I did and use a site that has already dealt with the problems of handling CSP reports at scale, something like https://report-uri.com/. You can start at the free tier and then pay for more reports and additional layers of security like data-watch which allows you to monitor for data egress and helps alert to potential Magecart-like attacks.

Test

Regardless of how complicated your site is I would then set up CSP in report-only mode. You do this by setting the Content-Security-Policy-Report-Only header with your expected directives and the reporting endpoint you set up.

If you are using Netlify like me then you can do this by adding a headers section like below to your netlify.toml file, if you have not already it’s also a good opportunity to add some other useful security headers.

[[headers]]
    for = "/*"
    [headers.values]
        X-Frame-Options = "DENY"
        X-XSS-Protection = "1; mode=block"
        X-Content-Type-Options = "nosniff"
        Content-Security-Policy-Report-Only = "object-src 'none'; default-src 'none'; manifest-src 'self'; connect-src 'self'; script-src 'nonce-$$NONCE_VALUE_PLACEHOLDER$$' 'unsafe-inline'; style-src 'self'; frame-ancestors 'none'; base-uri 'none'; report-uri https://jakebwellblog.report-uri.com/r/d/csp/enforce; img-src 'self'; require-trusted-types-for 'script';"

When you view your site and check the console you will see a bunch of violations. Lets resolve them.

Nonces

A key requirement for implementing strict CSP is nonce (pseudo-random number used once) attributes on script elements. As this blog is built using Hugo and served via Netlify this required a novel solution.

First, you will need to generate your nonce for each page load and inject it in both the headers and your response where necessary. We can do this using Edge Functions.

Add the following code to netlify/edge-functions/csp-nonce.js

export default async (request, context) => {
  // Get the response
  const response = await context.next();

  // Generate nonce, see https://scotthelme.co.uk/increasing-entropy-in-our-csp-nonces/
  const array = new Uint8Array(18);
  crypto.getRandomValues(array);
  const nonce = btoa(String.fromCharCode(...crypto.getRandomValues(array)));

  // Get existing headers
  const cspReportHeader = response.headers.get(
    "Content-Security-Policy-Report-Only",
  );
  const cspHeader = response.headers.get("Content-Security-Policy");
  const regex = /\$\$NONCE_VALUE_PLACEHOLDER\$\$/ig;

  if (cspReportHeader) {
    // Replace the nonce placeholder for reporting header if it is set
    response.headers.set(
      "Content-Security-Policy-Report-Only",
      cspReportHeader.replaceAll(regex, nonce),
    );
  }

  if (cspHeader) {
    // Replace the nonce placeholder for CSP header if it is set
    response.headers.set(
      "Content-Security-Policy",
      cspHeader.replaceAll(regex, nonce),
    );
  }

  if (response.headers.get("content-type").includes("text/html;")) {
    // HTML content needs nonce placeholder replaced
    const page = await response.text();
    return new Response(page.replaceAll(regex, nonce), response);
  }

  // Return unmodified response body as non HTML content
  return response;
};

Then configure the edge function to run on all paths by adding this section to your netlify.toml

[[edge_functions]]
function = "csp-nonce"
path = "/*"

Now you simply need to add your placeholder to script elements and they will be automatically replaced. For example

{{ $script := .Site.Data.webpack.main }}
{{ with $script.js }}
<script nonce="$$NONCE_VALUE_PLACEHOLDER$$" src="{{ relURL . }}"></script>
{{ end }}

Important: The placeholder value needs to be kept secret otherwise an attacker could potentially inject a script with the placeholder and get the correct nonce.

Further tweaks

Work through your violations and make tweaks to your implementation and/or your CSP policy as needed.

Depending on how complex your site is, it may be better to have different configurations for different pages. Think about where sensitive data is processed (sign in and sign up for example) and focus on those pages having as strict as possible setups. Use csp-evaluator to check any changes you make to your CSP policy as you go.

Deploy, Monitor and Refine

Once you have a clean console now it’s time to deploy. Push to production but ensure you keep CSP in report only mode. Let this sit for a while, monitor any reports that come in and fix issues. When you feel confident enough to enforce, remember to keep an eye on the violation reports and particularly keep an eye on them when you make changes.