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
’snonce
argument should default totrue
ifscript_src :strict_dynamic
is in place -
javascript_include_tag_tag
’snonce
argument should default totrue
ifscript_src :strict_dynamic
is in place - We can pass
nonce: false
to 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: true
to every single script – it’s automatic - Reduced chance of error, as failing to append
nonce
would 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