I've long been a proponent of Content Security Policies (CSPs). I've used them to fix mixed content warnings on this blog after Disqus made a little mistake, you'll see one adorning Have I Been Pwned (HIBP) and I even wrote a dedicated Pluralsight course on browser security headers. I'm a fan (which is why I also recently joined Report URI), and if you're running a website, you should be too.
But it's not all roses with CSPs and that's partly due to what browsers will and will not let you do and partly due to what the platforms running our websites will and will not let you do. For example, this blog runs on Ghost Pro which is a managed SaaS platform. I can upload whatever theme I like, but I can't control many aspects of how the platform actually executes, including how it handles response headers which is how a CSP is normally served by a site. Now I'm enormously supportive of running on managed platforms, but this is one of the limitations of doing so. I also can't add custom headers via Cloudflare at "the edge"; I'm serving the HSTS header from there because there's first class support for that in the GUI, but not for CSP either specifically in the GUI or via custom response headers. This will be achievable in the future via Cloudflare workers but for now, they have to come from the origin site.
However, you can add a CSP via meta tag and indeed that's what I originally did with the upgrade-insecure-requests implementation I mentioned earlier when I fixed the Disqus issue. However - and this is where we start getting into browser limitations - you can't use the report-uri directive in a meta tag. Now that doesn't matter if all the CSP is doing is upgrading requests, but it matters a lot if you're actually blocking content. That's where the real value proposition of a CSP lies too; in its ability to block things that may have been maliciously inserted into a site. I've had enough experience with breaking the CSP on HIBP to know that reporting is absolutely invaluable and indeed when I've not paid attention to reports in the past, it's literally cost me money.
Which brings me to how we're addressing this and Scott Helme's blog post yesterday on the launch of Report URI JS. What this boils down to is plugging your entire CSP into a meta tag (obviously without the report-uri directive) then using the SecurityPolicyViolation JavaScript event to identify when the violation occurs, grab the relevant info from it and post it over to an endpoint you define yourself. You can read Scott's post for the full details but in short, it means this for troyhunt.com:
- I created a CSP and inserted it as a meta tag in the head of the page using Ghost's code injection feature
- I also declared some JSON in the head of the page to configure how I want reporting via Report URI JS to be handled
- I embedded the Report URI JS library to bind it all together (it's served via a CDN and I'm using subresource integrity)
And now I have a full CSP with full reporting capabilities! That's the raw mechanics of adding one to the page, let me now go beyond that and talk about some of the other changes I had to make to actually get the site into a state where adding a policy was feasible.
Firstly, I created a baseline CSP with the help of the CSP Fiddler Extension. This is an awesome free tool that you install into Fiddler then just browse your site. It actually adds a couple of "report only" CSPs with (almost) nothing in them to whatever site you're browsing then sets a report-uri directive pointing back to the Fiddler proxy. What it means is that as you browse around a site (and hit any page or feature which might change the CSP), the extension builds up a policy based on actual reports submitted by the browser. There's no faster way to generate a baseline policy than this!
Next, I wanted to refine the policy because it was a bit too liberal in some ways and incomplete in other ways. For example, it was allowing unsafe-inline scripts which is precisely the sort of hole that XSS attacks exploit in the first place so I removed those and replaced them with SHA-256 hashes (one is Google Analytics in the head, the other is a bunch of scripts I inlined for performance reasons). I also added a connect-src of troyhunt.report-uri.com because Report URI JS makes an XMLHttpRequest when it lodges the report and added a script-src of cdn.report-uri.com which is where Report URI JS is loaded from.
Once I had my CSP right, I wanted to see how it performed. Now, in a world where I could control response headers I would have added it as a "report only" CSP but because I can only modify the HTML and report only isn't supported in meta tags, that wasn't going to work. Instead, I used Fiddler script to add it on the fly at the local proxy level using the following code:
if (oSession.HostnameIs("www.troyhunt.com")) { oSession.oResponse.headers["Content-Security-Policy"] = "[csp goes here]"; }
And then I literally just browsed around the site with Fiddler open and the console snapped over to another window then clicked links, used features such as the newsletter signup down the bottom and made sure there were no errors. And, of course, there were errors. For example, Disqus was attempting to use unsafe-eval which I later discovered was due to having this option checked:
This is the feature that lists other discussions on my site under the ones about the current page. Oddly though, after disabling then re-enabling the option it appeared to no longer use eval which promptly solved that problem.
Another reoccurring issue was the presence of onClick events on some tags. I removed those and replaced them with event listeners declared in the JS that's in the source of this page and is whitelisted by the SHA-256 hash script source.
There were a few similar minor issues and frankly, the fixes usually amounted to what we'd consider to be cleaner code anyway. Once I was happy all that was solid, I pushed it out and by virtue of this page now working for you (I assume it's working given you've read this far!), it's pretty much job done.
Having said that, I will have missed a few things. There's going on 9 years of blogs spanning hundreds of individual posts and there will be the odd CSP violation here and there. But that's the joy of reporting - I'll keep monitoring them in Report URI and when I see one pop up I'll give it a quick fix. Yes, it will mean that something will likely have broken in order to cause the report to be generated in the first place, but it'll be something non-critical like an image embedded from another host.
Lastly, for those of you wanting to follow this path, you must report on CSP violations. Not only is it essential to know if your CSP is inadvertently blocking something it shouldn't be, you absolutely want to know when something unintended is injected into your site by someone else - that's the whole point of CSPs! Of course, we'd love you to use Report URI to do this (you get 10k free reports per month which will give you a really good idea of what's happening), but even if you roll your own reporting endpoint, just make sure you have something that lets you keep track of those violations.