security

Some notes on setting up a CSP on a legacy site

Setting up a Content Security Policy (CSP) for a legacy site comes up with its own challenges, this article contains some tips that I have used.

Shaun Wilde
A cork board with a post-it note with a lightbulb.

Nonces instead of Hashes

Using hashes for small sites is okay when you have a small number of scripts and styles from internal and external sources but quickly becomes unwieldy when working with a large and the better approach is to use nonces. Since a nonce needs to be applied afresh each time a page is served then a placeholder is often used in your code and then replaced by server processing e.g.

<script nonce="**CSP_NONCE_PLACEHOLDER**" src="https://xyz.com/script.js"></script>
<script nonce="**CSP_NONCE_PLACEHOLDER**">
  ...
</script>
<style nonce="**CSP_NONCE_PLACEHOLDER**">
  ...
</style>

NGINX

A lot of sites use nginx and it is simple to replace the placeholder using a sub filter

set $cspNonce "${request_id}";
sub_filter_once off;
sub_filter_types *;
sub_filter **CSP_NONCE_PLACEHOLDER** $cspNonce;

The generated nonce can then be used later on when creating the Content-Security-Policy header.

Nonces and external ...

... scripts

The strict-dynamic keyword can be used with nonces to trust a script and simplify your CSP as you can ditch the domain whitelists (for script-src only).

... code snippets

Over the years your legacy code base has probbaly acquired a lot of code snippets especially by tracking networks that are so useful to help you understand your users e.g. googletagmanager. At the time these scripts were pasted into your pages they were probably ignorant of nonces and so will stop working properly when they now try to create a <script /> element. Since then google have created a nonce aware script that is compatible with the latest CSP version. Comparing the scripts (old vs new) the following line of code was added, to get a nonce from the page and apply it to the <script /> tag being created

var n=d.querySelector('[nonce]');
n&&j.setAttribute('nonce',n.nonce||n.getAttribute('nonce'));

Now a lot of these type of scripts (same era) use the same pattern/technique to inject a <script /> block onto your page and it is possible to tweak the above line for each scenario.

Note: This may be less of an issue nowadays due to cookie consent policies as these snippets are often refactored out to loaded only when the site visitor has consented to their use.

Inline scripts

It is common to execute JavaScript code on user actions e.g.

<select onchange="doSomething();">
  ...
</select>

To avoid supplying hashes for every snippet they can be refactored to use an inline script i.e.

<select id="picker">
  ...
</select>

<script nonce="**CSP_NONCE_PLACEHOLDER**">
document.getElementById("picker").addEventListener('change', () => {
  doSomething();
});
</script>

JavaScript URLs

Using a JavaScript URL on the href of an anchor (<a />) tag was often used to emulate a button but without all the default styling that came with a button and then using a library like JQuery to attach the proper event handler e.g.

<a id="clickme" href="javascript:void(0);" >Click Me!</a>

The recommendation is to convert them into button elements and attach the event via an event listener (as above) which is also better IMO for accessibility. If there are 100s (or 1000s) of these then this might be a mammoth task and so the first instinct maybe to use a hash of the script i.e.

'sha256-rRMdkshZyJlCmDX27XnL7g3zXaxv7ei6Sg+yt4R3svU=' # javascript:void(0)
'sha256-kbHtQyYDQKz4SWMQ8OHVol3EC0t3tHEJFPCSwNG9NxQ=' # javascript:void(0);

And there are articles that indicated that this is (or was) possible but browser compatibility isn't comprehensive. Alternatives such as using href="#!" may work better.

I've personally found that replacing the href attribute with a role with appropriate styling is cleaner and also improves accessibility e.g.

<a id="clickme" role="button" class="btn">Click Me!</a>

What else?

More to be added as and when...

Photo by AbsolutVision on Unsplash