A reasonable default CSP for Rails projects

Updated . Posted . Visible to the public. Repeats.

Every modern Rails app should have a Content Security Policy enabled.

Very compatible default

The following "default" is a minimal policy that should

  • "just work" for almost all applications
  • give you most of the benefits of a CSP

In your config/initializers/content_security_policy.rb, set

Rails.application.config.content_security_policy do |policy|
  policy.object_src :none
  policy.script_src :unsafe_eval, :strict_dynamic, :https # Browsers with support for "'strict-dynamic'" will ignore "https:"
  policy.base_uri :self

  # policy.default_src :self
  # policy.style_src :self
  # policy.font_src :self, :data
  # policy.img_src :self, :data
  # policy.connect_src :self, *(["ws://localhost:#{Rack::LiveReload::BodyProcessor::LIVERELOAD_PORT}/livereload"] if defined?(Rack::LiveReload))
    

  # Specify URI for violation reports
  # if Rails.env.staging? || Rails.env.production?
  #   policy.report_uri "/csp-violation-report-endpoint"
  # end
end

# Use nonces for script tags.
Rails.application.config.content_security_policy_nonce_directives = %w[script-src]

# Avoid changing nonces for the same session to not break e-tags.
Rails.application.config.content_security_policy_nonce_generator = lambda do |request|
  (request.session.id || SecureRandom.base64(16)).to_s
end

Then, find all occurences of javascript_include_tag, javascript_tag, javascript_pack_tag etc. and add nonce: true.

Also, make sure you don't have any other inline scripts in your application that do not use javascript_tag.

Why this works

The main point of this CSP is to disallow user content to insert any kind of script tags. All script tags (whether inline or with src) will now need an unguessable nonce attribute.

If any of your JavaScript inserts a script tag programmatically (for example a third-party chat widget or similar), the strict-dynamic will permit this.

Livereload

Any setup using rack-livereload will probably break, since the livereload JS is loaded without nonces. You can replace it with livereload-js, see our Livereload + esbuild guide as an example.

Possible improvements

unsafe_eval

Try removing the :unsafe_eval from your script source. Whether this is a problem depends on whether any JS library uses it. It is okay to remain there if required.

style_src

Try adding in the policy.style_src :self.

There is a chance this will break because some JS library uses inline styles incorrectly.

As a reminder, this will disallow

  • inline styles you set in your html
  • inline styles set by JS libraries using
    element.style = 'display: none;'
    

What will keep working is

element.style.display = 'none'

default_src / connect_src

Try adding in the other commented lines. default_src (or if set connect_src) would disallow XHR / fetch requests to other domains.

Note that in development this could break some "Live Reload" solutions; you can see the required config for Rack::LiveReload above.

CSP reporting

If you are still worried that your CSP will break something, you can set an URI to report CSP violations. In this case, every time a browser refuses to do something because of a CSP, it will contact the URI which has to implement a specific API Show archive.org snapshot .

You can even configure Rails to not enforce a CSP, but only send reports for violations:

config.content_security_policy_report_only = true

If you use Sentry Show archive.org snapshot you can use their endpoint, see the documentation Show archive.org snapshot for details.

Tobias Kraze
Last edit
Tobias Kraze
License
Source code in this card is licensed under the MIT License.
Posted by Tobias Kraze to makandra dev (2023-11-15 09:47)