Content Security Policy is an incredibly powerful security feature but in some circumstances it can be a little difficult to deploy. Removing inline scripts or styles often comes up as one of the hurdles. Here's how I introduced CSP nonce support in Nginx to counter the problem.
Content Security Policy
If you want more information on CSP then you can check out my blog, Content Security Policy - An Introduction, for more details. In short, CSP gives us a way to control the content that can be loaded into our pages by the browser and one of the common problems is removing inline scripts and styles. This is what I hope to address in the blog.
CSP Nonce
For CSP to be more effective any inline style or script has to be externalised. Inline script like this can't be used:
<script>
alert("Hello World!!!");
</script>
Instead you would have to move it out into a JS file and load it that way:
<script src="/js/alert.js"></script>
In this scenario the browser now knows that the script was definitely inserted by the host as it is being loaded from their domain:
<script src="(https://scotthelme.co.uk)/js/alert.js"></script>
This can be quite difficult to do and can also take a lot of time depending on your site. For this reason a new feature was introduced in CSP 2 to address the problem, nonce-source. Instead of having to externalise all of these scripts and styles you can now whitelist them using a nonce instead. You inject the nonce into the CSP header and then into the script or style tag:
Content-Security-Policy: default-src 'self' 'nonce-abc123'
<script nonce="abc123">
alert("Hello World!!!");
</script>
The same inline script can now be whitelisted using the nonce
attribute so that the browser knows it is safe for execution. The nonce has to be cryptographically random and strong enough to prevent an attacker being able to abuse them. They should be randomly generated per page load and at least 128 bits long before encoding. Obviously, the example I gave above of abc123
is in no way appropriate for use.
Building Nginx with nonce support
To create nonces in Nginx and use them in my pages I'm going to use two modules. The first is the ngx_set_misc module from OpenResty specifically for set_secure_random_alphanum which 'Generates a cryptographically-strong random string <length>
characters long with the alphabet [a-zA-Z0-9]
'. The second is the Nginx http_sub_module which is a 'filter that modifies a response by replacing one specified string by another'. I'm going to create a folder to download all of the necessary components and build Nginx. You need to update all of the following commands with the latest versions of each. Here are the links for Nginx, the Nginx Development Kit and the nginx_set_misc module so you can get the latest.
mkdir ~/nginx
cd ~/nginx
wget http://nginx.org/download/nginx-1.11.3.tar.gz
tar -xzvf nginx-1.11.3.tar.gz
wget https://github.com/simpl/ngx_devel_kit/archive/v0.3.0.tar.gz
tar -xzvf v0.3.0.tar.gz
wget https://github.com/openresty/set-misc-nginx-module/archive/v0.31.tar.gz
tar -xzvf v0.31.tar.gz
Now that you have all of the appropriate source you need to grab your current NginX configuration arguments.
scott@scotthelme:~$ nginx -V
You should get some output from this command and one of the sections is your configure arguments
, take a copy of it. You will need to make some additions to this to add in our new modules and don't forget to update the paths.
--add-module=/home/scott/nginx/ngx_devel_kit-0.3.0
--add-module=/home/scott/nginx/set-misc-nginx-module-0.31
This will add in the ngx_set_misc module and the NDK but there is one thing left. Check your existing configure arguments for the following and add it if it is not present. This is the Nginx http_sub_module that we need.
--with-http_sub_module
Once everything is ready you can build Nginx with the new modules.
cd ~/nginx/nginx-1.11.3
./configure [configure arguments]
make
sudo make install
Using nonces for CSP
Now that it's up and running we need to update our config to create the nonce that we will use for CSP and then inject it where we need. First of all we can add the nonce attribute to our script and link elements. Simply add the attribute and set the value to a string that we can filter on and replace with our actual nonce value later. In this example I will be using **CSP_NONCE**.
<script nonce="**CSP_NONCE**">
alert("Hello World!!!");
</script>
Once the nonce placeholder values is in place, we need to generate an actual nonce value and substitute it in the page. Creating the nonce itself is easy and requires only a single line of config. I placed this in the server block where I intended to use the nonce.
set_secure_random_alphanum $cspNonce 32;
This will generate the cryptographically secure random string that we want which is 32
characters in length and stored in a variable called $cspNonce
. Next, we need to substitute our nonce placeholder value in the page with our actual nonce value.
sub_filter_once off;
sub_filter_types *;
sub_filter **CSP_NONCE** $cspNonce;
This instructs the the Nginx http_sub_module to replace all occurrences of **CSP_NONCE**
in the page with the contents of the variable $cspNonce
. If you save and reload this config now you should be able to see that in action. Look at the source of your page and find the nonce value, if you refresh the page, you should see it change on each refresh. If the nonce value is not working and you still see your placeholder text, it could be the case that your upstream is using gzip compression and Nginx is not seeing the raw page. If this is the case you need to tell Nginx to pass the Accept-Encoding header to the upstream and disable compression wherever you proxy the request to the upstream.
proxy_set_header Accept-Encoding "";
If you need to do this, make sure that Nginx is setup to gzip responses so that you aren't sending uncompressed pages. You can do this in the Nginx config or the appropriate vhost file in the server context.
gzip on;
gzip_disable "msie6";
gzip_proxied any;
gzip_vary on;
gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript text/js application/javascript;
gzip_static on;
With those changes made, save and reload the config again to see if it works. You should now have a random nonce in the nonce attribute on each page load. All that's left to do now is hook this into our CSP header.
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-$cspNonce' always;
Remember, the header must be added after you have created the $cspNonce
variable so that it exists, but you should now be able to see the nonce value in the headers on your responses. The following is a snippet from my config showing the appropriate values and a test header called X-Nonce
if you want to see it in action without delivering a CSP header itself.
proxy_set_header Accept-Encoding "";
...
proxy_pass http://localhost:2368;
...
set_secure_random_alphanum $cspNonce 32;
sub_filter_once off;
sub_filter_types *;
sub_filter **CSP_NONCE** $cspNonce;
...
add_header X-Nonce $cspNonce;
You can check the response headers on my page to see the header in action, I will try to leave it there long after this blog is published and following is my CSP nonce placed in the page. I will include my CSP nonce value below and it should be replaced by an actual nonce value. Refresh the page to see it change.
JIYr435smMmKG1nAAFNlrKUewAEaTWt1
You now have the ability to inject a cryptographically secure nonce into your CSP headers and elements on your pages!
Update 16 Jan 2017:
A few people on Twitter have pointed out that I wasn't clear enough about the fact that the nonce substitution value has to be kept a secret. If an attacker finds out this value they can use it to inject into their own script tag and Nginx would rewrite a valid nonce into it for them. This value must be kept secret.
Feedback
If you get CSP setup with nonce support on your site then please let me know in the comments below. Likewise, if you have any improvements on the method I've suggested here or better ways to accomplish the same goal then please do leave details below, it'd be great to hear from you. With the ability to use a nonce to eliminate pesky inline scripts or styles, hopefully it will be a lot easier to deploy CSP on your site.