Journal tags:nonce

1

Securing client-side JavaScript

I mentioned thatI overhauled the JavaScriptonThe Sessionrecently. That wasn’t just so that I could mess about withHTML 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 mycontent security policywasn’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 withscript-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 calleda 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 hasan attribute callednonce.

It’s working a treat.The security headers for The Sessionare looking good. I have some more stuff in my content security policy—check out the detailsif you’re interested.

I initially thought I’d have to make an exception forthe 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 onThe Sessiondon’t have any inline scripts. For a while,everypage had an inline script in theheadof 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 calledrequiresJSthat 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 thehasJSclass isn’t set, hide any elements with therequiresJSclass.

I decided to switch over to usinga 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 putthe 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 thatcouldexecute JavaScript, that would be worse.