Securing client-side JavaScript
I mentioned that I overhauled the JavaScript on The Session recently. That wasn’t just so that I could mess about with HTML web components. I’d been meaning to consolidate some scripts for a while.
Some of the pages on the site had inline scripts. These were usually one-off bits of functionality. But their presence meant that my content security policy wasn’t as tight as it could’ve been.
Being a community website, The Session accepts input from its users. Literally. I do everything I can to sanitise that input. It would be ideal if I could make sure that any JavaScript that slipped by wouldn’t execute. But as long as I had my own inline scripts, my content security policy had to allow them to be executed with script-src: unsafe-inline
.
That’s why I wanted to refactor the JavaScript on my site and move everything to external JavaScript files.
In the end I got close, but there are still one or two pages with internal scripts. But that’s okay. I found a way to have my content security policy cake and eat it.
In my content security policy header I can specifiy that inline scripts are allowed, but only if they have a one-time token specified.
This one-time token is called a nonce. No, really. Stop sniggering. Naming things is hard. And occassionally unintentionally hilarious.
On the server, every time a page is requested it gets sent back with a header like this:
content-security-policy: script-src 'self' 'nonce-Cbb4kxOXIChJ45yXBeaq/w=='
That gobbledegook string is generated randomly every time. I’m using PHP to do this:
base64_encode(openssl_random_pseudo_bytes(16))
Then in the HTML I use the same string in any inline scripts on the page:
<script nonce="Cbb4kxOXIChJ45yXBeaq/w==">
…
</script>
Yes, HTML officially has an attribute called nonce
.
It’s working a treat. The security headers for The Session are looking good. I have some more stuff in my content security policy—check out the details if you’re interested.
I initially thought I’d have to make an exception for the custom offline page on The Session. After all, that’s only going to be accessed when there is no server involved so I wouldn’t be able to generate a one-time token. And I definitely needed an inline script on that page in order to generate a list of previously-visited pages stored in a cache.
But then I realised that everything would be okay. When the offline page is cached, its headers are cached too. So the one-time token in the content security policy header still matches the one-time token used in the page.
Most pages on The Session don’t have any inline scripts. For a while, every page had an inline script in the head
of the document like this:
<script nonce="Cbb4kxOXIChJ45yXBeaq/w==">
document.documentElement.classList.add('hasJS');
</script>
This is something I’ve been doing for years: using JavaScript to add a class to the HTML. Then I can use the presence or absence of that class to show or hide elements that require JavaScript. I have another class called requiresJS
that I put on any elements that need JavaScript to work (like buttons for copying to the clipboard, for example).
Then in my CSS I’d write:
:not(.hasJS) .requiresJS {
display: none;
}
If the hasJS
class isn’t set, hide any elements with the requiresJS
class.
I decided to switch over to using a scripting media query:
@media (scripting: none) {
.requiresJS {
display: none;
}
}
This isn’t bulletproof by any means. It doesn’t account for browser extensions that disable JavaScript and it won’t get executed at all in older browsers. But I’m okay with that. I’ve put the destructive action in the more modern CSS:
I feel that the more risky action (hiding content) should belong to the more complex selector.
This means that there are situations where elements that require JavaScript will be visible, even if JavaScript isn’t available. But I’d rather that than the other way around: if those elements were hidden from browsers that could execute JavaScript, that would be worse.