1

Securing client-side JavaScript

 1 week ago
source link: https://adactio.com/journal/21104
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Securing client-side JavaScript

May 5th, 2024

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.

10:43am

Tagged with thesession security frontend development javascript headers csp inline nonce

Older »

Have you published a response to this? Let me know the URL:

Responses

https://qubyte.codes/

If you want to avoid using a nonce value, the other way to go about it is to hash the resource and put that in the CSP header. I blogged about the interaction between CSP and import maps (which must be inline), and caching and it turns out to work really well: https://qubyte.codes/blog/progressively-enhanced-caching-of-javascript-modules-without-bundling-using-import-maps

# Posted by https://qubyte.codes/ on Sunday, May 5th, 2024 at 10:50am

Simon R Jones

@adactio content security policies can get very complex (especially for larger sites), nice to see this example.

Also not heard of the scripting media query - very neat.

# Posted by Simon R Jones on Sunday, May 5th, 2024 at 11:07am

2 Shares

# Shared by Simon R Jones on Sunday, May 5th, 2024 at 10:57am

# Shared by Andy Linton ✅ on Sunday, May 5th, 2024 at 10:57am

1 Like

# Liked by Simon R Jones on Sunday, May 5th, 2024 at 10:56am


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK