Hurrah! Sometimes it takes a little while for projects to make it through your backlog and into production, but the nonce-based policy for CSP on Report URI can now be crossed off the list!

Content Security Policy

If you're a regular reader here, you already know all about Content Security Policy, the one-stop-shop for mitigating a whole range of nasty application attacks including XSS (Cross-Site Scripting). Yes, there was the bogus lawsuit threats for using CSP recently, specifically CSP nonces(!), but overall CSP is an awesome piece of technology. You can use my quick reference cheat sheet to get started, use Report URI that I founded back in 2015 for reporting, and even protect against modern online threat likes Magecart with Script Watch and Data Watch! Today though, it's not about what you can do with CSP, it's about what we're doing with CSP!

CSP host-based lists

One of the common issues with CSP is maintaining the host-based list of allowed sources to load resources from. Take the script-src directive as an example and let's look at my script-src:

script-src 'self' 'report-sample' disqus.com c.disquscdn.com platform.instagram.com cdnjs.cloudflare.com scotthelme.disqus.com a.disquscdn.com go.disqus.com platform.twitter.com cdn.syndication.twimg.com syndication.twitter.com gist.github.com/ScottHelme/ static.cloudflareinsights.com js.stripe.com https://unpkg.com/@tryghost/;

That's a relatively simple example as I don't load scripts from too many places, but as your application gets more complex, so does managing this list with both adding new entries and removing old ones. The list on Report URI is a little simpler as we self-host many of our assets, and it looks like this:

script-src cdn.report-uri.com api.stripe.com js.stripe.com static.cloudflareinsights.com;

Crucially though, our CSP doesn't allow the execution of inline script and that saved us last year in our annual penetration test when the tester found an XSS issue that they couldn't exploit due to our CSP!

CSP Nonces

Using a CSP nonce is a slightly different way of allowing JavaScript to load and it can be very helpful if you have inline JS which is slightly trickier with CSP. We use a Cloudflare Worker to inject our Security Headers and I even wrote a detailed blog post on how to do CSP nonces with a Cloudflare Worker too. As you can see back in that blog post I setup and tested nonces in a Content-Security-Report-Only header so it would be safe and if I made any mistakes, they wouldn't cause things to break.

After much testing, and a lot of delays over the last year, I've finally found the time to finish this project up and deploy an enforced policy with CSP nonces!

You can head over to the Report URI site or check our scan results on Security Headers to see the policy in action, or even head over to the dev tools or view source window on your favourite browser!

Nonces and hosts

You may notice, on closer inspection of our header, that we still have our host-based list alongside the declaration of our CSP nonce. Our host-based list is pretty small, and as a result we're happy maintaining it, but there are considerations about the approach. The CSP spec says:

Script elements with the proper nonce execute, regardless of whether they’re inline or external. Script elements without the proper nonce don’t execute unless their URLs are whitelisted. Even if an attacker is able to inject markup into the protected resource, the attack will be blocked by the attacker’s inability to guess the random value.

So there are four scenarios that we potentially need to worry about:

  1. An attacker injects an inline script tag with no nonce: It won't execute because we don't allow inline-script tags.
  2. An attacker injects an inline script tag with a nonce: It won't execute because the attacker won't be able to guess the nonce value.
  3. An attacker injects an external script tag with no nonce: It won't execute because the location of the script will not be in our allow list.
  4. An attacker injects an external script tag with a nonce: It won't execute because the attacker won't be able to guess the nonce value.

That gives us a pretty robust level of protection and our next step is to consider going further and breaking out the nonce in the script-src into its own CSP header.

Content-Security-Policy: script-src cdn.report-uri.com api.stripe.com js.stripe.com static.cloudflareinsights.com;

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

Right now, the attacker needs either the CSP nonce or an allowed host in the list, but with the host-based list in one header, and the nonce in another header, the asset being loaded would need to satisfy both policies which would mean the attacker would need to know the nonce and load the asset from an allowed host. This is explained in the CSP spec:

The impact is that adding additional policies to the list of policies to enforce can only further restrict the capabilities of the protected resource.

I'm pretty happy with where we are now but I'm always experimenting with ways to improve things so check back in the future to see where we're up to!