If you’re using a Content Security Policy (CSP) in your Rails app you probably already know how finicky things can get. One common annoyance you may run into is an issue where a script tag is added to the <head> over and over again with each page request. It’s no bueno, but there’s a way around it.

THE SCENARIO

Let’s assume the following things:

  • You’re leveraging some rules in config/initializers/content_security_policy.rb.
  • You’ve specifically uncommented the Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } line.
  • You’ve added nonce: true to a script_tag in the <head> of your document.
  • You’re using Turbolilnks

I’m assuming if you’re still reading this that you’re here for a reason and have a reasonable understanding of what’s being achieved by using an nonce strategy. However, if you’re really flying by the seat of your pants here, I’ll briefly explain.

The above scenario means Rails is going plop a csp-nonce meta tag in the header. That tag will have a content value of some random string. We then set our CSP to explicitly allow the nonce. At this point any script tag that has the matching nonce value gets a pass. If a script tries to run without it (like someone being naughty) then it’s a no-go. Read more from Mozilla if you’re still curious.

Now, let’s say you’re including a third-party script of some kind in your <head>. Maybe it’s Google Analytics, Bootstrap, some fonts or something along those lines. You properly set nonce: true on the tag and start clicking around your site. If you’re peaking at your page source you’ll notice that script tag getting added again, and again, and again. Each time the nonce is updated to match the new meta tag value. The general principal of Turbolinks is that it doesn’t bother reloading the head on every request. This means it’s leaving our “old” script tags in place while it adds the “new” tag with the updated nonce. We don’t want this, so let’s clean it up.

THE SOLUTION

The fix here is not overly complicated. Something unwanted is getting left behind so let’s just clean it up. Luckily Turbolinks gives us a few events we can listen for to get the job done. I won’t presume to know your JavaScript setup, but the idea is that you’ll want to run something similar on every Turbolinks request.

document.addEventListener("turbolinks:visit", function() {
    // Grab a reference to the script tag
    const script = document.getElementById('scriptID')
    // Remove the script if it's present
    if (script) { script.remove() }
})

This allows us to remove the “old” script tag prior to Turbolinks finishing its business. After everything is done you should only be left with the “new” script tag with a correct nonce value. If I ever come across a more elegant way of telling Turbolinks to ditch expired nonce tags I’ll be sure to update, but this is simple enough for now.

I’m going to go pull some weeds in the yard now. Yay…

Written by Matt Haliski

The First of His Name, Consumer of Tacos, Operator of Computers, Mower of Grass, Father of the Unsleeper, King of Bad Function Names, Feeder of AI Overlords.