ruby-on-railscontent-security-policyturbolinksrails-ujs

Rails 5.2: Rails UJS, Turbolinks and CSP


We recently upgraded our Application to Rails 5.2. We are also using Turbolinks (together with the Rails Engine) and RailsUJS.

With Rails 5.2 we have new DSL for CSP (Content security policy). It is configured like this in initializers/content_security_policy.rb:

Rails.application.config.content_security_policy do |policy|
  policy.object_src  :none # disallow <object> tags (Good-bye Flash!)

  policy.default_src :self, :https
  policy.font_src    :self, :https, :data, Rails.configuration.application.asset_host, Rails.configuration.application.aws_s3_media_cdn
  policy.img_src     :self, :https, :data, Rails.configuration.application.asset_host, Rails.configuration.application.aws_s3_media_cdn
  policy.script_src  :self, :https, Rails.configuration.application.asset_host
  policy.style_src   :self, :https, Rails.configuration.application.asset_host

  if Rails.env.development? || Rails.env.test?
    # Fix for webpack-dev-server and ActionCable
    policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035", "ws://localhost:3000"
  end

  # Specify URI for violation reports
  # policy.report_uri "/csp-violation-report-endpoint"
end

# If you are using UJS then enable automatic nonce generation
Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) 

This works fine. But I cannot get Turbolinks and RailsUJS to work together.

In app/views/layouts/application.html.haml we have (simplified):

!!!5
%html
  %head
    = csrf_meta_tags
    = csp_meta_tag
    = javascript_pack_tag 'application', 'data-turbolinks-track': :reload
    = action_cable_meta_tag
  %body
    = yield

Assuming we have a ujs button like this:

<a class="btn btn-danger" data-disable-with="Processing..." data-params="device%5Bstatus%5D=inactive" data-remote="true" rel="nofollow" data-method="patch" href="/devices/1">Shutdown</a>

And a controller looking very simple:

class DevicesController < ApplicationController
  respond_to :html

  #...
  def update
    @device.update(device_params)

    respond_with @device, location: :devices
  end
end

This works fine and we get a proper response in browser coming from the Turbolinks rails engine:

Turbolinks.clearCache()
Turbolinks.visit("http://localhost:3000/devices", {"action":"replace"})

But we get the error:

Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' https: 'nonce-QAz+FlHz5wo0IwU5sIMZ/w==' 'nonce-IsrK1b0jw1w7cqRhHeZ7ug==' 'nonce-Rpl8hMBgap79cfwdlXXwjA==' 'nonce-1Wq7MbBEYMDCkEWGexwQ9Q==' 'nonce-EUL22iiKHn0hkNuW3fpkbA==' 'nonce-F5Vg50g0JvAvkXHHu+p0qw==' 'nonce-slHxjCy9VVEvvoIcJ870lg==' 'nonce-lboTgbdLG4iCgUozIK4LPQ==' 'nonce-K9yAPOgjZDXRTvnJb3glTA==' 'nonce-ux2kfUZjU/nxJn30LaTFjQ==' 'nonce-8E8cTAX+jWNpvl5lw0Ydjw==' 'nonce-BvJ4wU3AqjZRWY930+W8kg==' 'nonce-PsS01n7AnjmiThKQJFzUBA==' 'nonce-RCoOSLXbx6Cj8aw+LuBSwA==' 'nonce-o5MfDl/crSPzjSyMzIvXNA==' 'nonce-s8NPaOETMpU2f48LR2SuqQ==' 'nonce-Omuo2P68l09PTBFxmk4DkA==' 'nonce-N3YQfaIuPSrURB8jhVz3Sw==' 'nonce-Ts4Bp4GUqawLcHI1mRLcxw==' 'nonce-fTZ6W2u9eh8K5yCJMPfJGg==' 'nonce-1ST0058rq41fDhw8CforxA==' 'nonce-tA+jUJ1x841ZseUUjvQn9w==' 'nonce-CVjBLiByDSqBNHdG6/izBA==' 'nonce-1z6mH6xtPajsxVmojM8SNA==' 'nonce-0zlDfL8I0go9aII/DGZUzg==' 'nonce-WOrw4qdxeKfZQ1R7xUfdbQ==' 'nonce-G8kmjJA5E35Asgy6mj80PQ=='". Either the 'unsafe-inline' keyword, a hash ('sha256-9KVlOPCQBe0v+kIJoBA6hi7N+aI2yVDUXS9gYk4PizU='), or a nonce ('nonce-...') is required to enable inline execution.

So the first question here: Where are those many nonces coming from?

Another investigation: After I call Turbolinks.visit in the javascript console, the csp-nonce and csrf-token values are changing. I think that has to be the problem. Since Rails UJS uses the original value to add nonce to the inline javascript tag it creates. But after everything gets rendered the nonce has changed. So the inline tag is no longer valid. How can I avoid this?

-- NOTE: We are setting up Turbolinks and RailsUJS via webpacker like this:

import Turbolinks from 'turbolinks'
import Rails from 'rails-ujs'
// Start application
Rails.start(); // Rails ujs
Turbolinks.start(); // Turbolinks, obviously...

Thanks in advance,
spa


Solution

  • Follow up:

    The multitude of nonces was a bug in rails that has been corrected in Rails 5.2.1.