This article is a continuation to a series on security headers. Previous parts:

Content Security Policy (CSP) allows you to define what resources are allowed to load on a website's page. So for example if you expect that the only place you load JavaScript from is:

  1. Your app (e.g. https://yourdomain.com/scripts/scripts.js)
  2. Third-party libraries from a CDN (e.g. https://somejs-cdn.com/jquery/jquery-2.0.0.min.js)

You can specify a CSP rule that makes the browser block any scripts that try to load from anywhere else.

This header could look like this:

Content-Security-Policy: script-src 'self' https://somejs-cdn.com

In addition to controlling where scripts can be loaded from, you can control:

  1. CSS
  2. Images
  3. AJAX
  4. Web sockets
  5. Iframes
  6. Among some others... You can find more details on the available directives on https://content-security-policy.com/

Implementing CSP in ASP.NET Core

Now it's not hard to inject a simple header through a middleware. But with CSP it gets very difficult to maintain very fast.

My little Security Headers library contains a middleware for CSP that makes it quite a lot easier. Just install the library, and you can add something like this to your Startup.cs:

app.UseCsp(csp =>
{
    csp.AllowScripts
            .FromSelf()
            .From("ajax.aspnetcdn.com");
    csp.AllowStyles
            .FromSelf()
            .From("ajax.aspnetcdn.com");
});

It uses a fluent API so you can quite easily adjust the CSP header to what you want. The way it is setup, it'll only allow scripts and CSS from its own domain and ajax.aspnetcdn.com.

Creating a CSP rule set

It can be quite hard to figure out every single domain that should be allowed. An easy way to gather the set of rules you will need is to set the CSP to block all resources, and set it to report-only mode. Like so:

csp.ByDefaultAllow.FromNowhere();
csp.SetReportOnly();

Now the browser won't block invalid resources. Instead it will complain about them in your browser's F12 tools. You can go there and see what resources would have been blocked, and build up your rules. It is probably a good idea to leave it in report-only mode for a while in production as well to make sure you didn't miss anything. Setting up a report URI with csp.ReportViolationsTo("/csp-report"); is a good idea as well.

Inline scripts and CSS, nonces

If you want to allow inline scripts or styles, you have to use the 'unsafe-inline' directive. As the name says, it is unsafe. If you have a cross-site scripting vulnerability on your page, someone could inject an inline script to the page and the CSP would allow it. Sadly with some third-party libraries this must be enabled as you can't affect them.

But you can do something about your own inline scripts and CSS. Instead of adding the unsafe-inline directive to the CSP, we can add a nonce to it. Something like this:

<script nonce="abc123">
</script>

Then your CSP should look something like:

Content-Security-Policy: script-src 'self' 'nonce-abc123'

This allows the above inline script while still blocking inline scripts that were injected on the page. Now it is critical that the nonce is only used once. Otherwise it kind of defeats the purpose.

Nonces for scripts and CSS is another thing I included in the library as I figured this would be non-trivial.

Adding nonces to inline scripts and CSS

In Startup.cs, find the ConfigureServices method and add the following:

services.AddCsp(nonceByteAmount: 32);

This adds the necessary service for generating nonces per request to the service collection. It also specifies the nonce length as 32 bytes. You can specify anything you think is appropriate for your application. Note you can't use nonces with the library without this.

You will also have to configure the CSP middleware to include nonces for scripts, styles, or both.

app.UseCsp(csp =>
{
    csp.AllowScripts
            .FromSelf()
            .AddNonce();
    csp.AllowStyles
            .FromSelf()
            .AddNonce();
});

Find your _ViewImports.cshtml (normally in the Views folder), and add the following:

@addTagHelper *, Joonasw.AspNetCore.SecurityHeaders

Now we just need to mark the script and style elements with an attribute:

<script asp-add-nonce="true">
    console.log("Test");
</script>
<style asp-add-nonce="true">
    h1 {
        font-size: 20px;
    }
</style>

And that's it! Now every request gets a unique nonce in the header, as well as a unique nonce in the script/style tag.

One thing that should be noted about the nonce approach is that you can't cache all of the HTML output. Since the nonce is generated per-request, it has to generate script/style element also per-request.

Conclusions

Content Security Policy can certainly be useful for a web application's security as one of the many layers. It can be used to prevent clickjacking and execution of injected scripts.