In my effort to benchmark inlining vs linking the CSS on this blog, I noticed I had errors in my JS console pulling up my blog in production.

Content Security Policy: The page’s settings blocked the loading of a resource at inline (“script-src”).

Right. Inline scripts, kind of bad. Right. What was I inlining, anyway?

<script type="text/javascript">
  window.addEventListener("DOMContentLoaded", (ev) => {
    document.querySelectorAll("table").forEach((table) => {
      table.classList.add("pure-table")
    })
    document.querySelectorAll("img").forEach((img) => {
      img.classList.add("pure-img")
    })
  })
</script>

Styling applied to tables and images. Nothing crazy.

Nothing that I would want a draw to wait for. (more on that later)

Options, fixes

I could add a nonce to the <script> element and put the same nonce in the CSP. I think I would need a new nonce every build, but it’s not a big ask to write if there’s not already a plug-in.

I could add a shasum of the script to the CSP. The shasum could either be generated by hugo as part of the build or I could do it by hand, just once (surely).

I could also disallow inline scripts entirely, and put the script in a separate resource, linked and whitelisted in the CSP. This would be slower overall, but technically might even seem faster, since the script would just be deferred, and would hardly cause much of a redraw. The point of these scripts is to style tables & images (that’s it!).

A reasonable low-effort choice

Since this stuff doesn’t really need the urgency of inlining, I can definitely place it in a different script, mark it async, and add that script by hand to the CSP.

On the other hand, I think the shasum idea is really cool, so I’ll just do it by hand1.

Hugo plug-in?

It’s possible there’s some functionality in Hugo that takes a partial.js.html, strips the <script> tag and shasums the script inside, whitespace included. Then it could append that shasum to the Content-Security-Policy: ... script-src sha256-abc123xyz.

Are there any such plug-ins?

There’s a blog post by Jeremy Likness, but not much along the lines of what I want. That’s okay. I don’t really even want someone to make it easy to dump inline scripts on the page; it’s just not a good idea in general so it should maybe be a little painful to generate this crap.

Some help along the way

I found this blog post by Michael Van Delft really helpful in generating the ~base64(sha256(..)) of the script. Basically, you should run this on just the script body .. found inside the <script>..</script>.:

*openssl dgst -sha256 -binary script.min.js openssl enc -base64*

The Google CSP Evaluator is also really helpful in suggesting what I should add that my barebones CSP lacked.

Dev vs Prod

I do have to now keep 2 versions, one ready for development and one for production. My Production CSP disallowed the livereload inline script, so I couldn’t see updates as I wrote them.

Fortunately I can comment out the dev line and it won’t make it to the Production HTML as a comment.

Even better, I can add the 'unsafe-inline' I need in development with a templated if:

..script-src 'self'  {{ if eq hugo.Environment" "development" }}'unsafe-inline' {{ end }} }}..

But it doesn’t quite work

I can’t get both Safari & Firefox to not complain. Firefox wants it

    script-src 'strict-dynamic' 'sha256-M+38qX8qs/+oYc1vp4hO6pV1b52dr5R1XG1Iotu9Iro=' 'unsafe-inline'

but Safari complains if you single-quote 'strict-dynamic'—if you don’t leave the quotes off:

The source list for Content Security Policy directive ‘script-src’ contains an invalid source: ‘‘strict-dynamic’’. It will be ignored.

Whatever! I can live with some Safari errors. They’ll come around.

Final product

Spaced out, for clarity

    <meta http-equiv="Content-Security-Policy" 
      content="
        script-src

          {{ if eq hugo.Environment "production" }}'strict-dynamic' {{ end }}
          'sha256-M+38qX8qs/+oYc1vp4hO6pV1b52dr5R1XG1Iotu9Iro='
          'self'
          {{ if eq hugo.Environment "development" }}
            'sha256-aVazutaOCLIAK6U8or7MmKB8HiWGu++aucAQROMajGY=' 
            'sha256-qfzS8wll/NrJ7tpJ+l/Tk0F6iRZ/l/9g1xQrVARO2fo=' 
            'unsafe-inline'
          {{ end }}

          https:
          http:;
        object-src 'none';
        base-uri 'none'
      ">

Some notes:

  1. 'strict-dynamic' doesn’t jive with Hugo’s livereload.js, so I make sure that only ever shows up on a production build (hugo -e production)

  2. the M+38.. shasum is for the script I inline

  3. the aVa.. shasum is for the script snippet

  1. as usual, it was a bit harder than expected, but worth it