If you’ve ever worked with me or spoken with me about web application security, you’ll know that I love CSP - probably to an unhealthy degree. I think the idea of a server announcing what type of content is going to be delivered is fantastic.

Just to quickly go over it, in case your memory needs a refresher, CSP or “Content Security Policy” is a HTTP response header, that instructs the client from which kind of sources they’re allowed to load which kind of data.

For example, the following Policy:

default-src 'self';
script-src 'self' https://code.jquery.com https://cdn.jsdelivr.net;
style-src 'self' https://cdn.jsdelivr.net;
object-src 'none'

By default, only data from your own domain may be loaded. Scripts can only be loaded either from your own domain, from code.jquery.com (for jQuery) or from cdn.jsdelivr.net (for Bootstrap). CSS can also only be loaded from your own domain or cdn.jsdelivr.net. And any kind of Flash object or Java applet is explicitly disabled.

This seems like a pretty reasonable policy, right? Well, it’s already insecure. cdn.jsdelivr.net hosts JSONP and Angular libraries, allowing an attacker to bypass the CSP and execute malicious JavaScript.

‘strict-dynamic’ to the rescue

‘strict-dynamic’ was supposed to be the saviour, descending from heaven to redeem our souls and free us from the clusterfuck we have created. But if that were the case, I wouldn’t be writing this shit, would I?

So the idea is that you write a “loader script”, which then inserts all the required scripts into the DOM for you. Something like this:

var s = document.createElement('script');
s.src = "https://cdn.example.com/some-script-you-need.min.js";
document.body.appendChild(s);

The idea is that trust would be propagated, meaning that if the inserted script would create, say, an event listener, that event listener would be trusted and executed.

So why doesn’t this work? This seems like a good idea. Well…

Third-Party Code and its Consequences

Imagine there is a tool you really like, such as Jellyfin. You host it on your server, then make a nice reverse proxy to it. Now you decide you want a CSP for it. So, guess what it will look like?

default-src 'none';
img-src 'self';
style-src 'self' 'unsafe-inline';
script-src 'self' https://www.gstatic.com;
connect-src 'self';
font-src 'self' data:;
manifest-src 'self';
media-src 'self' blob:;
worker-src 'self' blob:

That’s a lot of stuff. A lot more than we had previously. And worst of all, it’s still unsafe, because www.gstatic.com hosts Angular or JSONP endpoints. But what can we do? Not a whole lot. Since Jellyfin isn’t directly under our control, we can just work with what we have.

And that is really the crux of the argument. A lot of web applications are either based on a third-party framework or include third-party modules, which can’t easily be modified to be compatible with ‘strict-dynamic’. As a result, you have the following options:

  • Ignore CSP entirely
  • Implement CSP in an insecure way (‘unsafe-inline’ & ‘unsafe-eval’)
  • Bother developers to design their code to be CSP compatible and fail
  • Cry

Aside from that, you really don’t have that many options. Even if you want to implement CSP correctly, you often can’t. Compare that to HSTS, which basically requires you to ensure that you can serve via HTTPS, and you can see plain as day why CSP isn’t adopted nearly as widespread as HSTS.

How to fix this?

I…don’t have a solution. I really don’t. The web is a mess and it gets messier every day. I could say that in the future, all web frameworks would be designed to be CSP-friendly, but both you and I know that that’s a pipe dream.

So if you’re an admin, I really won’t blame you if your CSP looks like garbage. Mine does to. The difference is, your boss pays me to tell you that yours looks shit, while yours doesn’t pay you to tell me that mine looks shit.