Turbolinks broken by default with a secure CSP

  • Good: Rails includes built-in tools to generate a CSP

  • Great: That CSP encourages disallowing unsafe evaluation of inline JS

  • Incredible: Rails includes javascript_tag(nonce: true) helper so you can include nonced inline JS

  • WTF If you use all these tools together with Turbolinks none of the nonces work.

If you want UJS, Turbolinks, and other inline nonced JS to work you need to do the following:

  1. Change Nonce generation so that nonces do not change for turbolinks requests (as the DOM is not updated)
# In config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy_nonce_generator = -> (request) do
  # use the same csp nonce for turbolinks requests
  if request.env['HTTP_TURBOLINKS_REFERRER'].present?
    request.env['HTTP_X_TURBOLINKS_NONCE']
  else
    SecureRandom.base64(16)
  end
  1. Inject a header into turbolinks requests so the above nonce generation code works
// Somewhere in /app/javascript
document.addEventListener("turbolinks:request-start", function(event) {
  var xhr = event.data.xhr;
  xhr.setRequestHeader("X-Turbolinks-Nonce", $("meta[name='csp-nonce']").prop('content'));
});
  1. Because nonces can only be accessed via their IDL attribute after the page loads (for security reasons), they need to be read via JS and added back as normal attributes in the DOM before the page is cached otherwise on cache restoration visits, the nonces won’t be there!
// Somewhere in /app/javascript
document.addEventListener("turbolinks:before-cache", function() {
  $('script[nonce]').each(function(index, element) {
    $(element).attr('nonce', element.nonce)
  })
})

All of this is outlined in the following Turbolinks issue https://github.com/turbolinks/turbolinks/issues/430

As a Rails user, I expect that as long as I stay on the well-trodden path, the tools should work together harmoniously. The CSP nonce generation should work with Turbolinks and UJS out of the box.

I suspect many Rails apps do not enable a Content Security Policy and that makes me sad, and makes me question if it should be on by default. IMO, the best time to create a solid CSP is at the start of a new app, not later on in the apps lifecycle when you have code that is dependent on insecure practices.

As an experienced dev, I came up with a solution so that I could keep Turbolinks in my project and use JS nonces with a CSP, but I could see a new person swearing off Turbolinks after the experience, assuming it just isn’t supported enough to remain in the project, especially since it is conflicting with a critical security feature like CSP.

10 Likes

I wonder if it would be better to fix Turbolinks to update the nonce on the script tags rather than make Rails generate script tags that are compat with previous requests. I did a rough monkey-patch against Turbolinks 5.2 (prior to the typescript conversion) that looks basically like this:

cspNonce = null
window.addEventListener 'load', ->
  cspNonce = document.querySelector("meta[name='csp-nonce']").getAttribute('content')

Turbolinks.Renderer.prototype.createScriptElement = (element) ->
  if element.getAttribute("data-turbolinks-eval") is "false"
    element
  else
    createdScriptElement = document.createElement("script")
    createdScriptElement.textContent = element.textContent
    createdScriptElement.async = false

    # Inline `copyElementAttributes`. Set old nonce
    for {name, value} in element.attributes
      if name is 'nonce'
        createdScriptElement.setAttribute(name, cspNonce)
      else
        createdScriptElement.setAttribute(name, value)

    createdScriptElement

Most of this is copy/paste from Turbolinks. The main thing done here is:

  1. Save the nonce from the meta tag as it was when the first full page load happened.
  2. When “activating” each script tag use the old nonce instead of the new one.

This means no special paths on the server for Turbolinks. Turbolinks is picking and choosing what it wants from the new response as always. This also seems in line with Rails UJS that does something similar:

https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee#L70

The only issue is if you are using SJR this might break UJS as the meta tag is still updated. Turbolinks might need to be changed to not update the CSP meta tag.

6 Likes

that’s a really interesting solution, @e_a! Any interest in upstreaming it?

1 Like

Yes, I can look into putting together a PR based on this.

4 Likes

Thank you for your solution. I spent around 15-20 minutes before I started searching for a solution. For some reason, I felt Turbolinks was the culprit so I narrowed down the search quickly and found your posting. Thank you again.

I’ve been fighting this issue for days, if you want to have a “secure” CSP, and not use unsafe-inline, you MUST disable turbolinks entirely. It’s wild to me that more people are not encountering this issue.

1 Like

This actually still does not work in edge rails. Very bummed I yet again cannot use turbolinks with a project before running into issues with the stack fighting itself.

Just FYI, I had decided I’m not going to spend the time to try to improve this in Turbolinks now that it is moving to legacy software and being replaced by Turbo. I haven’t used Turbo yet but if it has the same issues with CSP I suggest it be addressed there.

One of the motivations behind developing Turbo was secure CSPs not allowing inline javascript. See: Turbo Handbook

2 Likes

FYI - This is still an issue with Turbo (which replaces Turbolinks). I’ve documented a variation of the solution in this issue.

1 Like

@terracatta Thank you! As soon as I hit this, I figured this must have been hit before and google’d for it and found your solution. I’m amazed there isn’t a fully supported solution as well, but yours works like a dream!

1 Like