I am using "Turbolinks 5.1.0".
#Gemfile
gem 'turbolinks', '~> 5.1'
#Layout header
<%= javascript_include_tag "application", nonce: true %>
#config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
#Google chrome console error
VM32 application-ae291f799496478302742f713e72f20a7958b7077387b87e18ab98c51ec979c4.js:243
[Report Only] Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' https: 'unsafe-inline' 'nonce-UiVx2CiP0HHN9jOOSEG43g=='". Note that 'unsafe-inline' is ignored if either a hash or nonce value is present in the source list.
n.assignNewBody @ VM32 application-ae291f799496478302742f713e72f20a7958b7077387b87e18ab98c51ec979c4.js:243
n.replaceBody @ VM32 application-ae291f799496478302742f713e72f20a7958b7077387b87e18ab98c51ec979c4.js:243
(anonymous) @ VM32 application-ae291f799496478302742f713e72f20a7958b7077387b87e18ab98c51ec979c4.js:243
t.renderView @ VM32 application-ae291f799496478302742f713e72f20a7958b7077387b87e18ab98c51ec979c4.js:243
n.render @ VM32 application-ae291f799496478302742f713e72f20a7958b7077387b87e18ab98c51ec979c4.js:243
t.render @ VM32 application-ae291f799496478302742f713e72f20a7958b7077387b87e18ab98c51ec979c4.js:243
e.renderSnapshot @ VM32 application-ae291f799496478302742f713e72f20a7958b7077387b87e18ab98c51ec979c4.js:243
e.render @ VM32 application-ae291f799496478302742f713e72f20a7958b7077387b87e18ab98c51ec979c4.js:243
t.render @ VM32 application-ae291f799496478302742f713e72f20a7958b7077387b87e18ab98c51ec979c4.js:243
(anonymous) @ VM32 application-ae291f799496478302742f713e72f20a7958b7077387b87e18ab98c51ec979c4.js:243
(anonymous) @ VM32 application-ae291f799496478302742f713e72f20a7958b7077387b87e18ab98c51ec979c4.js:243
1) Use the data-turbolinks-track: reload
<%= javascript_include_tag "application", 'data-turbolinks-track': :reload, nonce: true %>
OR
2) Reuse the same nonce using session-store for turbolink requests, is this correct way of solving this problem?
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.session["mykey"].present?
request.session["mykey"]
else
request.session["mykey"] = SecureRandom.base64(16)
end
end
Please suggest the correct solution here!
I have a similar issue when requesting a .js.erb template after a Turbolinks visit.
I use rails-ujs (5.2.1) and turbolinks (5.2.0) and I have the csp_meta_tag set in my layout head.
What I understand of the problem, is that after a Turbolinks visit, the csp-nonce meta tag isn't updated in the DOM. Then, if a request to a .js.erb template is made, the js code will be injected inside an inline script by the rails-ujs library but with an outdated csp-nonce content.
A solution is to avoid to regenerate a csp nonce when it is a turbolinks request (what you did in your second solution). Then the csp-nonce content in the DOM and the one used server side stay synchronized.
Your first solution works, because setting "data-turbolinks-track": :reload and nonce: true together force a page reload on every turbolinks visit (as the javascript tag changed with the nonce). Which update the csp-nonce content but break the Turbolinks "magic".
However, it feels like a better solution would be to update the csp meta tag after a turbolink visit, but I have no solution for this yet.
I'm adding Turbolinks to a legacy Rails app and I see the same issue on my side.
It's weird because even though Chrome says it blocks the script, I can see that it executes - try to console.log from the script.
However, it feels like a better solution would be to update the csp meta tag after a turbolink visit, but I have no solution for this yet.
The meta tag is actually updating. Try $("meta[name='csp-nonce']").prop('content') on different page visits and verify that it's changing.
@parasharrk did you manage to solve it?
Edit: Let me describe my configuration in more details.
# layout:
<%= csp_meta_tag %>
<%= javascript_include_tag :vendor, 'data-turbolinks-track' => true, defer: true %>
<%= javascript_include_tag :store, defer: true, 'data-turbolinks-track' => true %>
The CSP meta tag changes after each Turbolinks visit, but Chrome treats the nonce from the initial page load as the correct one.
I wanted to post an update here in case some people hit the same issue. I've spent more time on this than I wanted.
If you work on a legacy Rails app with inline scripts and use CSP with a nonce approach, you will hit two issues:
SecureRandom.base64(16)) won't work well with Turbolinks. What you want to do is use the same approach as rails-ujs uses: reuse the nonce from the meta tag:document.addEventListener("turbolinks:request-start", function(event) {
var xhr = event.data.xhr;
xhr.setRequestHeader("X-Turbolinks-Nonce", $("meta[name='csp-nonce']").prop('content'));
});
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
end
This approach is better than the one described above (with a session key). The approach with the session key breaks when the user is using multiple tabs/windows.
<meta name="turbolinks-cache-control" content="no-cache">
thanks for the updated workaround @madejejej 馃憤
You'll have to disable caching history for pages that use inline scripts. I didn't dig too deep into why though, so if someone has an explanation or solution, I'll be happy to hear it.
@madejejej This is just a guess but I noticed that nonces get removed from script tags in the DOM after the page is loaded (is this some kind of browser security feature?).
Is it possible turbolinks is caching the DOM with these nonces removed and therefore when it pops a page out of the cache the nonces aren't present and the js can't execute?
So I spent more time on this and my hacky solution to get @madejejej's answer working with the turbolinks cache was to do the following...
document.addEventListener("turbolinks:before-cache", function() {
$('script[nonce]').each(function(index, element) {
$(element).attr('nonce', element.nonce)
})
})
This will add the nonces back to the DOM via their IDL attribute before the page is cached. This should be safe to do.
Hi I'm getting this issue and I have no inline css or anything. Any ideas?
Could it be the loading bar that turbolinks uses?
Perhaps this "workaround" should be a part of turbolinks and turbolinks-rails by default?
You can use this configuration in your content_security_policy file
if Rails.env.development?
policy.script_src :self, :https, :unsafe_eval, :unsafe_inline
else
policy.script_src :self, :https
end
You can use this configuration in your content_security_policy file
if Rails.env.development? policy.script_src :self, :https, :unsafe_eval, :unsafe_inline else policy.script_src :self, :https end
Thanks so much, @nsoseka
FYI this seems Chrome-specific. Firefox seems to update nonces appropriately.
FYI I've submitted this issue as a post in the Rails Forum's May of WTFs because this bug will bite all people using Turbolinks with a Secure CSP that doesn't allow unsafe eval of inline javascript.
IMO Turbolinks should work perfectly without any configuration, with the suggested policy (which should also be secure by default) in the default generated content_security_policy.rb
Any feedback about how much of your time was lost to this bug or your general thoughts are welcome!
https://discuss.rubyonrails.org/t/turbolinks-broken-by-default-with-a-secure-csp/74790
Most helpful comment
I wanted to post an update here in case some people hit the same issue. I've spent more time on this than I wanted.
If you work on a legacy Rails app with inline scripts and use CSP with a nonce approach, you will hit two issues:
SecureRandom.base64(16)) won't work well with Turbolinks. What you want to do is use the same approach as rails-ujs uses: reuse the nonce from the meta tag:This approach is better than the one described above (with a session key). The approach with the session key breaks when the user is using multiple tabs/windows.