Adding Variable CSP Nonces To Static Sites
We build all different sized sites, some call for a full blown CMS but others are more suited to a static approach. These static sites come with some unique problems when it comes to CSP and dynamically injected scripts.
The problem fundamentally stems from the way CSP works, it either needs to be given a full list of all scripts you expect to load or be given a nonce. Any script that has this nonce attached is allowed to run. So when we are asked to add something like Google Tag Manager whose whole purpose is to dynamically inject arbritary scripts the allow list approach went out the window and we had to use nonces. But it's a static site, nonces by definition should only be used once and then thrown away.
Enter Apache SSI
We use Apache to server all our sites, and it has some features which allow static sites to not be quite so static. Server Side Includes, or SSI, is just such a feature. At its simplest it allows you to inject text into a web page with special tags: <!--#echo var="DATE_LOCAL" -->
We can use this to inject nonces into script tags:
<script nonce="<!--#echo var=NONCE_VARIABLE_NAME -->">...</script>
So now we have a way of dynamically injecting nonces into script tags but we still need to be able to generate the nonce itself, there are two ways I've found of doing this and they trade off ease of use and security.
mod_unique_id
There is an Apache module called mod_unique_id, all it does makes a UNIQUE_ID
environment variable available. This module is widely available and easy to install, you probably already have it installed and just need to enable it. On Debian-like systems: a2enmod unique_id
. This allows us to supply the CSP header and scripts like this:
Header set Content-Security-Policy "script-src ... 'nonce-%{UNIQUE_ID}e'; ..."
<script nonce="<!--#echo var=UNIQUE_ID -->">...</script>
This approach is easy to implement but it's not without its problems:
- Unique !== random. The difference is subtle but important, mod_unique_id will generate a value which wont come up again but that doesn't mean you can't predict what the next one will be.
- It's not strictly base64 encoded. CSP requires a nonce to be base64 encoded, you can work around this using Apache expressions but it starts to get messy:
expr=script-src ... 'nonce-%{base64:%{reqenv:UNIQUE_ID}}'; ...
mod_cspnonce
A better approach is to use a purpose built module such as mod_cspnonce. It too simply provides an environment variable, in this case called CSP_NONCE
. The problem with this is you are going to have to compile it yourself from source, it's dead simple though. See the README for details. This allows us to supply the CSP header and scripts much like the previous example:
Header set Content-Security-Policy "script-src ... 'nonce-%{CSP_NONCE}e'; ..."
<script nonce="<!--#echo var=CSP_NONCE -->">...</script>
This module is built such that it uses a proper source of random data and also correctly base64 encodes the value.
Propagating Nonces To Injected Scripts
So far we've allowed scripts that were included in the source code as sent by Apache, but any scripts that are injected at run time are going to get blocked unless they too have the nonce. In our case we were adding Google Tag Manager to the site which has an alternate version which just extracts the nonce from the page and adds it to the scripts it injects. A shortened and de-obfuscated copy of the relevant parts:
newScript = document.createElement('script'); // create the new script element
var nonceElement = document.querySelector('[nonce]'); // get the nonce from any other element on the page
newScript.setAttribute('nonce', nonceElement.nonce || nonceElement.getAttribute('nonce')); // add the nonce to the new script
Alternatively 'strict-dynamic'
can be added to the CSP policy, this causes all injected scripts to inherit the policy from the script that injected them. The script doing the injection still needs the nonce however:
<script nonce="<!--#echo var=CSP_NONCE -->">
// inject new script
</script>
<script src="js/injector.js" nonce="<!--#echo var=CSP_NONCE -->"></script>
Conclusion
Even when building completely static sites Apache allows us to add back just a little dynamic content so we don't have to resort to doing something like adding an unsafe-inline
directive and completely compromising any benefit of CSP.