My blog

Configuring the Content-Security-Policy (CSP) Header

Summary ⇒ Setting up the Content-Security-Policy (CSP) requires a fine-tuned configuration to allow or block the loading of resources. Here, I cover my approach to set my CSP and the reasoning behind some of my choices.


Detailed explanations of what CSP is and how it works can be found on Web.dev, on Mdn web docs, on Scott Helme’s introduction and his CSP Cheat Sheet. But in summary, CSP is an HTTP header that controls what sources can be loaded onto a website and which can not. This means, that it needs to be set especially for each website, ensuring that the restrictions it sets do not break any functionality but that is still restrictive enough to protect against XSS vulnerabilities.

My Policy Creation Approach

1. Report Only

Initially, I did not include the CSP header, but its report version:

Content-Security-Policy-Report-Only

This Report-Only follows the same syntax and performs the same checks as the CSP but instead of blocking the loading of resources it just reports the violations and lets the website function normally. That is why, in this post I refer to both of them as CSP or policy, making no further distinction.

2. Block From the Outside

We can say that a website has two types of resources. Internal ones, which come from our website itself and are under our control, like local fonts, stylesheets, or scripts. And external ones, which are managed and stored somewhere else and that we just call and use on our end, like Google’s fonts or analytics.

Having that in mind and following the general idea presented by Black Hill, I started with a strict policy, just allowing internal resources to be loaded.

Content-Security-Policy-Report-Only: default-src 'self’;

[csp_violation.webp]

Initial CSP violations.

3. Whitelisting External Resources

As expected from the initial policy, rocket-loader.min.js, the Lato Font, and connections to Google Analytics are reported as they are external resources.

[external_resources_violation.webp]

Violations from external resources.

To handle this violation it is enough to specify the source list with all the sources/hosts that are not to be blocked. In this list we can be as granular as you need, specifying:

  • A subdirectory. Ex: https://cdnjs.cloudflare.com/ajax/libs/vue/

  • A specific resource. Ex: https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js

  • A specific endpoint without parameters. Ex: https://www.googletagmanager.com/gtag/js

As I have very few resources, my resulting source lists are easy ones:

script-src 'report-sample' 'self' https://www.googletagmanager.com/gtag/js;
style-src 'report-sample' 'self' https://fonts.googleapis.com/css;
connect-src 'self' https://region1.google-analytics.com/g/collect;

4. Allowing Inline Execution

Going back to the original violations we see that some of them refer the inline execution of internals and external resources. This can be confusing as our policy should only block external resources. Nevertheless, inline execution is not considered a best practice for multiple reasons, security being one of them, and so is blocked by CSP by default.

[inline_violation.webp]

Inline Violations.

When it comes to inline execution the general recommendation is to not have it and instead to move these resources to their own files. I like this idea but it is not always applicable, be it because of performance or refactoring limitations.

In my case, as some resources are autogenerated by Jekyll, refactoring the code would require changes to GitHub’s CI/CD process, a complication that I had rather avoid. Therefore, I decided to whitelist specific inline executions using the approaches already included in CSP.

These CSP whitelisting approaches are:

  • Unsafe-eval: allowing any text-to-JavaScript mechanisms like eval().

  • Unsafe-inline: allowing any inline script or style.

  • Hashes: allowing a script or style that matches a pre-calculated hash value.

  • Nonces: allowing a script or style that contains a given Nonce-Value in its script or style tag. Ideally, a randomly generated value for each request.

As my site is static and I am not likely to make changes to any of these resources, the easiest way for me to whitelist them is by using the hash of each resource, keeping in mind that if their source code changes, the hash will need to be changed too.

script-src 'report-sample' 'self' 'sha256-CwE3Bg0VYQOIdNAkbB/Btdkhul49qZuwgNCMPgNY5zw=' 'sha256-4ZPsBzAj9gP0zFvccWBCEa3czx2vtHZiNMmXaNl8CTs=' https://www.googletagmanager.com/gtag/js;

style-src 'report-sample' 'self' 'sha256-CwE3Bg0VYQOIdNAkbB/Btdkhul49qZuwgNCMPgNY5zw=' 'sha256-cvSpgqcvrX5FXBN95EEaqOipPecSDs2cmymOp7JgmH8='

5. Reporting CSP Violations

To set up reporting I had to:

  • Set two reporting directives in my CSP.

    report-uri https://caponte.report-uri.com/r/d/csp/enforce; report-to default;

  • Add an extra header to the transform rules described in my previous post about HTTP Security Headers.

    Report-To: {“group”:”default”, “max_age”:31536000, “endpoints”:[{“url”:”https://caponte.report-uri.com/a/d/g”}],”include_subdomains”:true}

[report_to_header.webp]

Report-To Header.

To handle these reports I am using report-uri.com, which’s free plan provides all I need to receive 10.000 reports a month and visualize the reports of the last 90 days.

[csp_reports.webp]

Example CSP Reports.

Final CSP

These previous steps took care of all violations so I went ahead and changed the report header for the active Content-Security-Policy, which resulted on:

default-src 'self';
script-src 'report-sample' 'self' 'sha256-CwE3Bg0VYQOIdNAkbB/Btdkhul49qZuwgNCMPgNY5zw=' 'sha256-4ZPsBzAj9gP0zFvccWBCEa3czx2vtHZiNMmXaNl8CTs=' https://www.googletagmanager.com/gtag/js;
style-src 'report-sample' 'self' 'sha256-CwE3Bg0VYQOIdNAkbB/Btdkhul49qZuwgNCMPgNY5zw=' 'sha256-cvSpgqcvrX5FXBN95EEaqOipPecSDs2cmymOp7JgmH8=' https://fonts.googleapis.com/css;
connect-src 'self' https://region1.google-analytics.com/g/collect;
font-src 'self' https://fonts.gstatic.com/s/lato/;
base-uri 'self';
object-src 'none';
report-uri https://caponte.report-uri.com/r/d/csp/enforce;
report-to default;

My CSP Explained

  • default-src ‘self’ still only allows internal resources from my website (but not its subdomains).

  • Scripts and Styles are limited to be either internal, matching the given hashes, or coming from Google Tag Manager or Google Fonts, respectively.

  • ‘report-sample’ indicates that in case of a directive violation a sample of the code will also be sent as part of the report.

  • Following some other hardening recommendations:

    • base-uri ‘self’ prevents changes to the location of scripts loaded from relative paths.
    • object-src ‘none’ blocks fetching and executing embedded plugin resources like flash.
  • Every time that a browser blocks a resource based on my CSP this event will be reported to my reporting URL. Here, the “default” value seen on the report-to directive refers to a definition found on the Report-To HTTP header

Final Notes

- Inline Resources

CSP’s default approach is to block the loading and evaluation of inline resources, which significantly hardens any website from XSS vulnerabilities. However, if your styles are forcing you to consider ‘unsafe-inline’ you might be interested in checking Mike West’s presentation and Scott Helme’s post Can you get pwned with CSS? to see two sides of this issue.

- A directive’s Default Behaviour

All directives that are not specified or that can not fall back to default-src, will automatically behave as if that directive was set to allow everything ( * ).

- CSP Generation and Evaluation

You can use tools like Csper or ReportURI’s tool to generate the policy for you and then fine-tune it to your needs. In the same way, you can use this CSP evaluator to get some extra feedback and ideas about your policy.

Carlos Aponte 12 Oct 2022 AppSec, HTTP-Headers