Skip to main content

Easy nonce-based Content-Security-Policy with Nginx

Content-Security-Policy is a powerful mechanism that can mitigate some of the web attacks, mostly related to user-generated content and vulnerable libraries. We publish a general guidance on deploying CSP based on our experience while developing this website, but here we would like to describe a simple trick we used to deal with a specific CSP usage scenario being whitelisting by nonce.

The nonce keyword is a powerful mechanism to whitelist inline scripts and styles. The idea is that a web application or web server declares the Content Security Policy that whitelists one particular pseudo-random value - the nonce:

Content-Security-Policy: default-src 'none'; script-src 'nonce-731ac2484429a72173467b14558f0343';

Per its name, the value should be used once, so should be unique at least per user session. The web application then outputs a HTML code, most likely rendered from a template. If it has the same nonce as declared in CSP header, the browser will render it:

<script nonce="731ac2484429a72173467b14558f0343">
...some inline JS...
</script>

If you try to implement both parts in the web application, you will quickly discover it effectively kills all of your preciously designed multi-layer caching: each HTML template needs to be rendered separately for each user session.

The trick is to still preserve all the caching and just inject the actual value of the nonce at the very last moment, just before the response leaves to the user - so in the web server. This is easily done with Nginx. Your HTML template does not substitute any variables for nonce on its own, it just contains a static placeholder with reasonably unique name:

<script nonce="a3a522aceed42a145ba15c1fe536a6519c8b0161eec810bb3a64aa996388e273">
...some inline JS...
</script>

Then your Nginx configuration uses sub filter to replace the placeholder in output HTML with a pseudo-random value unique for each user session:

load_module modules/ngx_http_subs_filter_module.so;
sub_filter_once off;
sub_filter a3a522aceed42a145ba15c1fe536a6519c8b0161eec810bb3a64aa996388e273 $ssl_session_id;

The $ssl_session_id is set by ngx http ssl module and satisfies all our requirements: is unpredictable and unique per user session. Now you only need to set the CSP header:

add_header Content-Security-Policy "default-src 'none'; script-src 'nonce-$ssl_session_id'";

The variable contains a session identifier of your TLS connection, which is a value derived from the session key that allows the connection to be re-established without costly public key operations using TLS connection caching.

Of course, it’s completely unrelated to CSP! It’s simply a semi-random string that is unique to each visitor and remains static for a short period of time. Most importantly, it’s the only variable exposed by Nginx that satisfies our requirements without resorting to third-party modules.

Find me on Fediverse, feel free to comment! See how