Stop Turbolinks from adding multiple script tags with nonce
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 ascript_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…