Dynamic Content Security Policy (CSP) with safe fallback

Content Security Policy (CSP) is something very powerful and can protect against several web client security issues but can also protect the users privacy. I've been working with CSP on a medium big website and implemented dynamic CSP for a few weeks now and want to share my experiences.

I've found a working solution on how you can implement dynamic CSP by using a safe fallback policy that will be used if a user escapes our rules.

If you don't know what dynamic CSP is, you can read my previous blog post. But the tl;dr is that dynamic CSP is when you're using more than one CS-policy on your website instead of using a global one. With dynamic CSP you can reduce the attack surface by very much.


If is [not] Evil

I'm using nginx because of the "more_set_headers"-module. With this module you can remove or add specific headers and this is exactly what we want. We want to remove the global CSP-header and then use a specific one that we built for our purpose. So there will be no place on the website where we are not using CSP.

According to nginx, if is evil. I do agree with them but in some cases you actually can and should use if. In this case you can because we will only check if the arguments exists on a certain location, and if it does we will use another CSP, and if it does not the [hacker] user will be delivered a very strict CSP that is more likely not exploitable.

In my case I will be using arguments(?foo=bar) and this is more risky than paths(/foo/bar) because you can have arguments anywhere in the URL, example:

Real URL: example.com/index.php?foo=bar&baz=foz
Still working URL: example.com/index.php?baz=foz&foo=bar

Of course you should always fix this issue in your code first because real security relies on the code, not the protection mechanisms. But that's out-of-scope this time.

So if we have the above example where index.php?foo=bar and index.php?baz=foz renders 2 different pages with completely different content you can do something like this with nginx:

server {
[...]
1. more_set_headers "Content-Security-Policy:[...]";
[...]

2. location ^~ index\.php {    
3.    more_clear_headers "Content-Security-Policy";    
4.    more_set_headers "Content-Security-Policy:[...]"

5. if ($args ~ ^foo=bar$) {
6.    more_set_headers "Content-Security-Policy:[...]"
   }

7. if ($args ~ ^baz=foz$) {
8.   more_set_headers "Content-Security-Policy:[...]"
   }     

}

1. This is the global CSP that is used in the server-class. I assume that you already have a header here.

2 - 4. If the location is index.php, we will remove the CSP-header that is in the server-class. However, all the other headers will be inherited. This CSP should be very strict because it will be used as fallback. ^~ matches only if the location starts with index.php

5. Here we will use if to check the arguments of index.php. The regular expression ~ is for case-sensitive matching. So fOO=bAr would result in that the fallback CSP would be in use. We also want to check that the whole argument starts and ends with foo=bar. If we don't do this it will be able to bypass the whole thing.

6. Here we will define our CSP for index.php?foo=bar. We don't need to use more_clear_headers here because we are setting the header, even if it already exists(it will be replaced).

7 - 8. Same as above.


This could be dangerous!

When I first started with dynamic CSP I got a few reports via my bug bounty program about [dynamic] CSP-bypasses so I've learnt a lot on my small journey so the same will most definitely go for you.

Depending on your code you could get different results from what I've got. You also need to know your expected arguments and queries. It's important that you evaluate if you really need a dynamic CSP and how it should work. For me, I wanted to remove the unsafe-* from as many places as possible so I started with dynamic CSP because I wanted to reduce the attack surface. The best thing would first be to manually change your code so that you don't need to use inline style or scripts, but in some cases that's not the goal and let me tell you why.

For instance, if you have a login-page you should always try to make it as strict as possible because that's where the user enters his credentials. Sometimes the issue is not web-based attacks, but the browser can also be injecting scripts to sniff credentials(trust me, I have seen this in the wild many times). So really, there's no reason not to be using a dynamic CSP.

Anyway, bypasses will most definitely be found if you're not testing your configuration out. Some of the bypasses I found was something like:

hello.php?baz=foz&foo=bar
hello.php?/hello.php/hello.php?foo=bar

The first bypass was due to bad regexp because nginx checked if there was any arguments containing foo=bar with $args ~ foo=bar and nginx will use the last argument. The fix was to use $args ~ ^foo=bar$ and then the fallback CSP would be in use if a hacker tried with baz=foz&foo=bar.

The second bypass was due to wrong regexp in the location-directive. location ~ /hello was used and this can be bypassed by doing something like hello.php?/hello.php. Fix this by checking if the location really starts with hello.php and not contains anywhere in the URL.


Why not use meta tags?

It really depends on your code if either you should use headers or meta tags for your CSP. If you ask me, meta tags are easier to use even if you're developing a dynamic CSP. The problem with meta tags is that you can't use a fallback CSP, even if you sent via headers because header will be applied before the meta tags.

It's recommended that you serve CSP via headers, but the main reason behind this I don't know.

If your site is not very dynamic I'd recommend using meta tags.


Can you hack this?

So I made a "challenge" where you can try out the above example. If you can bypass it I would be very impressed. In fact, I do believe that it's possible to bypass!

The bypass would simply be:

  • Run script on hello.php?foo=bar
  • Show an image on hello.php?baz=foz
  • Or be able to do something with the x-parameter

You can of course add anything after these but the URL must start with domain.com/hello.php?foo=bar or domain.com/hello.php?baz=foz. REMEMBER that you can use the x-parameter to insert an image or script anywhere on the page!



Here is the nginx configuration(cut for brevity):

more_set_headers "Content-Security-Policy: img-src 'none'; script-src 'none'";

location ~ hello\.php$ {

more_clear_headers "Content-Security-Policy";
more_set_headers "Content-Security-Policy: img-src 'none'; script-src 'none'";

if ($args ~ ^foo=bar$) {
   more_set_headers "Content-Security-Policy: img-src *";
   }

if ($args ~ ^baz=foz$) {
   more_set_headers "Content-Security-Policy: script-src 'unsafe-inline'";
   }

If you do want to try this out you can use my server: http://sweha.xxx/hello.php (I'm the owner, please see my Keybase). However, I'm not sure for how long I want to host this content so don't be alarmed if you see a 404(or even a 500).

If you succeed, feel free to tell me how via Twitter or mail.


Some last words

Getting dynamic CSP to work properly isn't easy. Regexp can be tricky and nginx does not always play well. But once you actually understand your code and how it renders with different parameters and queries you can build a very dynamic CSP.

Meta tags are always a good place to start first, but like I said before, it's not recommended.