Content Security Policy With Rails

My beloved Ruby on Rails framework offers a convenient way for managing the content security policy (CSP). Unfortunately, it's disabled by default. But then, even when we turn it on, the default settings are less than ideal.

No worries! In this article, we will configure CSP in Rails, adopt best practices, and ensure a more secure environment for your web application.

How to enable CSP?

If you create a new Rails application (at least in Rails 7), CSP is turned off by default. To enable it, we must uncomment the config/initializers/content_security_policy.rb and restart the server.

The important part looks like this. Note I added comments to explain each line.

config.content_security_policy do |policy|
  # fallback allows to load everything from any website
  # as long as it's https or our own domain.
  policy.default_src :self, :https

  # fonts can be loaded from any https websites,
  # or using data:
  policy.font_src    :self, :https, :data

  # same for images
  policy.img_src     :self, :https, :data

  # completely forbid <object>, <embed>,
  # and other legacy tags
  policy.object_src  :none

  # scripts and styles allowed to load from
  # any HTTPS website
  policy.script_src  :self, :https
  policy.style_src   :self, :https
end

It's alright. But it's far from ideal. They allow you to load everything from any website as long as it's HTTPS.

It's also worth noting that CSP will NOT allow any inline scripts in this default configuration. You'll have to put every script and style in its own file, and even specifying inline handlers like <button onclick="..."> is forbidden.

There's still a way to securely allow inline scripts - see the section about nonces.

Making it more secure

What if we want to get serious about CSP? First, it's worth understanding that :https directive is far too permissive. If you know exactly that all your scripts are coming from your own domain, it's better to make it stricter:

policy.script_src  :self

If some of your scripts or styles are loaded from third-party websites, specify those domains.

policy.style_src  :self, https://maxcdn.bootstrapcdn.com

It could be a bit trickier with fonts; see how to set up Google Fonts in this article.

To conclude, try to remove as many of those :https as possible and replace them with specific domains.

We can make it more secure in more ways, but it depends on your specific needs. Look at this article for more details.

Allowing inline JavaScripts with a nonce

If you need to use an inline script but don't want to sacrifice security - you can still do this.

The CSP specification allows using so-called nonces to make the inline scripts which are "safe".

Basically, a nonce is a random string you can put in the CSP config.

policy.script_src  :self, 'nonce-abc123'

Then, you specify it with the script.

<script nonce="abc123">
  alert("Hooray, I'm allowed!")
</script>

But with Rails, it becomes even more convenient. You don't need to worry about generating those tokens, Rails will do this for you.

To make it work, you need to add those lines (or uncomment, as they are already present in the file):

# Generate session nonces for permitted importmap and inline scripts
config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
config.content_security_policy_nonce_directives = %w(script-src)

The default strategy is to use the session ID for every script (meaning that all your inline scripts will share the same token), or you can use a random one like this:

config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }

I can't think of a single reason why one of the strategies would be more secure or less secure than the other. So feel free to use whatever is convenient.

Just don't forget to enable nonce for every inline script:

<%= javascript_tag nonce: true do -%>
  alert('Hello, World!');
<% end -%>

How to add a report url?

Finally, if you want to add a reporting url, that's also very easy with Rails. Just add this line into the configuration:

policy.report_uri "<your reporting url>"

For example, if you use CSPHero to collect and make sense of the CSP reports, it would look something like

policy.report_uri "https://app.csphero.com/report/P2viPU61"

Conclussion

As you can see, the magnificent Rails framework makes our life much easier with built-in support for CSP. I only wish it was enabled by default and use a bit more sane defaults. But I hope they will fix it in the upcoming releases.

Collect And Analyze
CSP Reports in Real-Time ✨✨✨