I've recently hit a few bumps with Safari whilst implementing an improved CSP on report-uri.io. This blog post is to outline the issues I've had so that others might be able to avoid them!
Versions and info
I'm using Safari 8.0.8 (10600.8.9) on OS X Yosemite 10.10.5 which is the only Mac hardware I have to hand for testing and an iPhone 4S running iOS 8.4 and Safari Mobile 8.0 too. Both of the issues below were verified against both of these platforms/versions.
Blocking source expressions without a scheme
One of the nice features of CSP is that you can specify a list of sources and not have to specify the scheme for that source. The browser should assume the scheme that the page was loaded with, so if you specify
fonts.googleapis.com and the page was loaded over https, the browser should allow you to load assets from
https://fonts.googleapis.com. This stops you having to specify https:// at the start of every source in your CSP, which could add quite a lot of unnecessary bulk. The browser should also allow you to upgrade the scheme if the page is loaded using http. That is,
https://fonts.googleapis.com would still be a valid source even if the page was loaded over http because upgrading to a secure scheme isn't really a problem. You can find this behaviour documented in the CSP specification under the heading Matching Source Expressions. The pertinent parts of that section are bullet point 4.5, 4.5.1 and 4.5.2 which outline the specifics.
Sometimes the spec can be a little hard to understand, so to translate:
5 - If the whitelisted host in the CSP does not have a scheme, the browser is allowed to:
1 - Loads assets using HTTP or HTTPS if the page is served using HTTP.
2 - Loads assets using HTTPS if the page is served using HTTPS.
The above behaviour seems perfectly reasonable and as you can imagine, if we're defining multiple hosts in multiple directives of our CSP, not having to add https:// to ever single one is certainly going to cut down on the size of the policy. Sadly, safari doesn't do this! In the below image you can see that Safari is blocking an asset being loaded from
https://fonts.googleapis.com even though
fonts.googleapis.com is present in the
style-src directive of the CSP, as indicated.
This was also the case for all of my other assets on the page and I started to get a lot of reports through report-uri.io at the same time.
Fortunately, I had always specified the scheme in my CSP but as the policy was starting to grow, removing the https:// from every source seemed like a logical step to cut down on the size of the policy. As soon as I pushed the changes into my report only policy (you can issue both a CSP and CSPRO at the same time for testing) the violations started coming in to my dashboard. Coupled with a few comments on Twitter that things didn't seem to be working properly on the test site, I'd soon identified the problem. For now, if I want my CSP to work properly in Safari, and if you want yours to work properly too, you will have to leave the scheme in the policy for each source you whitelist. Thanks Apple.
Enforcing a report only policy directive
The next problem is quite a serious one too, and it's that Safari enforces a sandbox directive from a report only policy that should never be enforced. It took a little more digging to figure this one out as it wasn't really what I was expecting to be the problem. I'd issued a much more stringent CSPRO header on the test site with the view of pushing it to the live site if it all worked out, but instead of just informing me if anything would go wrong, things did go wrong. The directive in question was the
sandbox directive and it allows you to force content into its own origin and prevent it from doing things like submitting forms, running script, navigating other browsing contexts etc... You can alleviate some of these restrictions by setting certain flags like
allow-scripts if you wish. On my test policy I'd enabled sandboxing and set the
allow-scripts flag as I'd like to be able to do both of those things. What I hadn't realised though was that I'd forgotten to add the
allow-same-origin flag, so none of my JS was going to be able to issue XHRs against my own domain. Oopsie! That said though, this is a report-only policy (CSPRO) header and this is exactly what it's for. You can issue a new policy, make a mistake and rather than things falling apart you will just get lots of errors in the console and loads of reports sent back to say 'hey, you screwed up'. Unless of course you're using Safari.
Safari takes the, admittedly wrong, policy settings and is actually applying them to the page! This should never happen for a report only header but it also should never happen for the
sandbox directive either. The specification clearly states that "The sandbox directive will be ignored when monitoring a policy".
The fact that this breaks the analyser is because the sandbox is forcing the script to be loaded into its own origin. When it then tries to make an XHR to the site, which normally would have been the same origin, it is now actually a cross-origin request. The request, instead of being from report-uri to report-uri, is now from null to report-uri. This origin is not present in the ACAO header and the request fails. The error message that you get from Safari really doesn't help in identifying the cause of the problem here and is pretty useless. Chrome on the other hand gives you a much better error which I generated by temporarily enforcing the misconfigured header in a live CSP.
Armed with this information I was able to identify the source of the problem and disabled the CSPRO header which fixed the site in Safari. Enforcing a directive in a report only policy is bad, but enforcing a directive that's not even supposed to be monitored in a report only policy is even worse. Update: It seems that Edge also mirrors this behaviour and enforces the sandbox directive in a report only policy.
How violation reporting helped
The reports that started flooding in for the problem with not specifying the scheme came in thick and fast. Virtually every asset on the page was creating a report because none of them were being loaded. Each page load requires many assets to be loaded and there are many page loads constantly taking place. This is a really great way to instantly know that something is wrong. The real time view on my CSP report dashboard went nuts and I knew it was time to start digging. Initially I thought I'd done something wrong but after checking a few local browsers and seeing that they weren't sending any reports (Chrome and Firefox) I knew I had to broaden my search. This has led me to realise that knowing which browser sent the reports that you're seeing could be quite important and is now something that I will be working on. A small icon, think of a favicon, by each report to show which browsers have reported that particular issue would be ideal. If you suddenly get an issue being produced by only one particular browser, or only mobile browsers for example, it can quickly narrow down the problem and allow you to resolve it faster. Hopefully I should be able to have something up and running on the test site in the next week or two. As for the sandbox issue, that's a bit more problematic because this particular issue didn't generate a CSP violation. The XHR failed because it broke the same origin restrictions. Yes, they came about because of the CSP, but it seems the browser was killing the request outside of the context of the CSP. This is where some more wide reaching QA would have picked up on the issue and I now have a MacBook Pro and an iPhone 4S handy for testing on Apple/Safari.