Hey everyone!
So I’ve been dealing with CSP a lot recently and I’ve got a quality of life proposal. I’d like to send a PR soon, but it seems like a significant one, so let’s discuss it.
Here’s the context:
Different content-security-policies provide a different level of security. Sometimes we’re content with just setting policy.script_src :self, :https and off we go. However, CSP evaluator and similar tools would advise against that policy, as the policy doesn’t protect us much. If we want to have maximum security regarding script execution, we need to use the 'strict-dynamic' modifier.
When strict-dynamic is in place, a browser would block every single script which isn’t explicitly allowed by the policy. Generally, there are two ways to mark a specific script as “allowed”:
- Provide a hash for the inline script:
policy.script_src :strict_dynamic, "'sha256-...'" - Append a nonce to
<script>'s HTML attribute
Nonces have been supported for in view helpers for a while now:
javascript_include_tag "https://some_remote.js/script.js", nonce: true
javascript_tag "https://some_remote.js/script.js", nonce: true do
# ...
end
Naturally, every single javascript_include_tag MUST be accompanied by a nonce, as hashes won’t work in that scenario. We’ll have to pass nonce: true to every single helper.
It’s a bit easier with javascript_tag, as we can calculate hashes and add them to the policy. However, I still pass nonces in 99% of cases.
So here’s the proposal:
-
javascript_tag’snonceargument should default totrueifscript_src :strict_dynamicis in place -
javascript_include_tag_tag’snonceargument should default totrueifscript_src :strict_dynamicis in place - We can pass
nonce: falseto disable them on a case-by-case basis - This behavior should be disabled by default, and only manually enabled in a config
Before:
# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
policy.default_src :self, :https
policy.script_src :strict_dynamic, "'sha256-VXTjzayMEyDRVN5a5QVhsNV+Dx7gMhiP7xY90/74XyQ='"
end
Views:
# app/views/**/*.html.erb
<%= javascript_include_tag "some_path", nonce: true %>
<%= javascript_include_tag "some_other_path", nonce: true %>
<%= javascript_include_tag "third_lib", nonce: true %>
<%= javascript_include_tag "third_lib", nonce: true %>
<%= javascript_tag nonce: true do %>
// this code has interpolation so we can't hash it, hence the nonce
this.key = "<%= some_interpolated_value %>"
<% end %>
<!-- this tag is hashed %>
<%= javascript_tag do %>
alert("Hello, world!")
<% end %>
After
# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy_append_nonces = true # new setting / name to be discussed
Rails.application.config.content_security_policy do |policy|
policy.default_src :self, :https
policy.script_src :strict_dynamic, "'sha256-VXTjzayMEyDRVN5a5QVhsNV+Dx7gMhiP7xY90/74XyQ='"
end
Views:
# app/views/**/*.html.erb
<%= javascript_include_tag "some_path" %>
<%= javascript_include_tag "some_other_path" %>
<%= javascript_include_tag "third_lib" %>
<%= javascript_include_tag "third_lib" %>
<%= javascript_tag do %>
// this code has interpolation so we can't hash it, hence the nonce
this.key = "<%= some_interpolated_value %>"
<% end %>
<!-- this tag is hashed, so we disable nonce %>
<%= javascript_tag nonce: false do %>
alert("Hello, world!")
<% end %>
TL;DR of the benefits
- No need to append
nonce: trueto every single script – it’s automatic - Reduced chance of error, as failing to append
noncewould result in a user-facing error - If developers don’t need the nonce for a rare case, they can pass
nonce: false - The new behavior is opt-in, so no breaking changes
- Future versions of Rails might make the behavior default
Let me know what you think! I’ll prepare a PR sometime soon