[Proposal] JavaScript tag helpers should add nonce if strict-dynamic policy is in place

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:

  1. javascript_tag’s nonce argument should default to true if script_src :strict_dynamic is in place
  2. javascript_include_tag_tag’s nonce argument should default to true if script_src :strict_dynamic is in place
  3. We can pass nonce: false to disable them on a case-by-case basis
  4. 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

1 Like