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.

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

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.

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

5 Likes

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

# Liked by Royce Williams on Sunday, May 5th, 2024 at 1:08pm

# Liked by creanium on Sunday, May 5th, 2024 at 2:04pm

# Liked by Albriggs on Sunday, May 5th, 2024 at 4:14pm

# Liked by shaunrashid on Sunday, May 5th, 2024 at 5:42pm

Related posts

My approach to HTML web components

Naming custom elements, naming attributes, the single responsibility principle, and communicating across components.

Pickin’ dates

HTML web components for augmenting date inputs.

Secure tunes

Closing a security hole on The Session.

Speedy tunes

Improving performance on The Session.

Relative times

Messing around with Intl.RelativeTimeFormat on The Session.

Related links

abc to SVG | CSS-Tricks

Aw, this is so nice! Chris points to the way that The Session generates sheet music from abc text:

The SVG conversion is made possible entirely in JavaScript by an open source library. That’s the progressive enhancement part. Store and ship the basic format, and let the browser enhance the experience, if it can (it can).

Here’s another way of thinking of it: I was contacted by a blind user of The Session who hadn’t come across abc notation before. Once they realised how it worked, they said it was like having alt text for sheet music! 🤯

Tagged with

Why your website should work without Javascript. | endtimes.dev

The obvious answer to why you should build a website that doesn’t need js is… because some people don’t use js. But how many?!

Tagged with

Ban embed codes

Prompted by my article on third-party code, here’s a recommendation to ditch any embeds on your website.

Tagged with

Tagged with

Ain’t No Party Like a Third Party - CSS-Tricks

Chris is doing another end-of-year roundup. This time the prompt is “What is one thing people can do to make their website bettter?”

This is my response.

I’d like to tell you something not to do to make your website better. Don’t add any third-party scripts to your site.

Tagged with

Previously on this day

2 years ago I wrote Even more UX London speaker updates

Irina Rusakova, Cennydd Bowles, Chris How, Lou Downe, and Giles Turnbull.

7 years ago I wrote Patterns Day speakers

The line-up is now complete.

9 years ago I wrote 100 words 044

Day forty four.

14 years ago I wrote Debatable act

Speaking against the Digital Economy Act.

15 years ago I wrote Hyphen Nation

Battling the emdashculation of the internet.

20 years ago I wrote Cinco de Mayo

Won’t somebody please think o’ the my-oh!

21 years ago I wrote Accessibility statement

I’ve updated the “About” section of this site to include a brief accessibility statement.