Skip to Content
Course content

238: Content Security Policy in Rails

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably heard that Rails is "secure by default," and for the most part, it is. But escaping HTML isn't a silver bullet. If a malicious actor finds a way to inject a script—maybe through a third-party library you're using—they can steal session cookies or redirect your users. That's where Content Security Policy (CSP) comes in. It's essentially a "guest list" for your browser: if a resource isn't on the list, the browser refuses to load it.

Why do I need this if Rails already handles XSS?

Think of Rails' built-in escaping as your front door lock. It's great, but what happens if someone climbs through a window? CSP is like having a security guard inside the house who checks IDs. Even if an attacker manages to inject a <script> tag into your page, the browser will look at your CSP and say, "Wait, this script is coming from evil-hacker.com, and that's not on my approved list." Then, it simply refuses to execute the code.

Where do I actually define these rules in Rails?

Rails provides a dedicated initializer for this. If you haven't already, you can generate it, but most of the time you'll just create config/initializers/content_security_policy.rb. You define your policy using a DSL that tells Rails which HTTP headers to send.

Let's say your app uses Google Fonts and a Stripe payment widget. Your config would look something like this:

Rails.application.config.content_security_policy do |policy|
  policy.default_src :self, :https
  policy.font_src    :self, :https, "https://fonts.gstatic.com"
  policy.img_src     :self, :https, "https://s3.amazonaws.com"
  policy.script_src  :self, :https, "https://js.stripe.com"
  policy.style_src   :self, :https, "https://fonts.googleapis.com"

  # Specify an URI for violation reports
  # policy.report_uri "/csp-violation-report-endpoint"
end

The :self keyword is crucial—it tells the browser that resources from your own domain are trusted. I usually start with default_src :self and then explicitly open up the specific holes I need for external APIs.

My inline scripts stopped working. How do I fix this without disabling CSP?

This is the most common point of frustration. By default, CSP blocks all inline scripts (like <script>alert('hi')</script>) because that's exactly how most XSS attacks work. You'll see a scary error in your browser console saying the script was blocked.

You could add :unsafe_inline to your script_src, but please don't. That basically defeats the purpose of having a CSP. Instead, use a nonce (a "number used once"). A nonce is a random string generated for every single request. If the script tag has the correct nonce, the browser lets it run.

First, tell Rails to use nonces in your initializer:

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

Then, in your layout file, add the CSP meta tag helper:

<head>
  <%= csp_meta_tag %>
  ...
</head>

Now, when you have an inline script you absolutely can't move to a separate file, you pass the nonce to it:

<script nonce="<%= content_security_policy_nonce %>">
  console.log("This inline script is now trusted!");
</script>

I'm terrified of breaking the site. Is there a "safe" way to test this?

I've been there. Pushing a strict CSP to production and realizing you've accidentally blocked your analytics, your payment gateway, and half your CSS is a nightmare. The solution is report_only mode.

In your initializer, you can tell Rails to send the Content-Security-Policy-Report-Only header instead of the enforcement header:

Rails.application.config.content_security_policy_nonce_generator = -> { SecureRandom.base64(16) }
Rails.application.config.content_security_policy do |policy|
  policy.report_only = true
  # ... your rules ...
end

When report_only is true, the browser won't actually block anything. Instead, it will just log a warning to the console (or send a JSON report to your report_uri) telling you what would have been blocked. I always run my app in report-only mode for a few days in a staging environment to catch all the edge cases before flipping the switch to full enforcement.




📋 Practical Task

Exercise: Hardening a Stripe-Integrated Checkout Page

You are working on a Rails app that integrates Stripe for payments and uses Google Fonts for branding. Currently, the app has no CSP, making it vulnerable to XSS attacks.

Your Goal: Configure a Content Security Policy that allows the following, while blocking everything else:

  • Resources from your own domain (:self).
  • Scripts from https://js.stripe.com.
  • Styles from https://fonts.googleapis.com.
  • Fonts from https://fonts.gstatic.com.
  • An inline script in the application.html.erb layout that initializes a tracking variable.

Requirements:

  1. Create or modify config/initializers/content_security_policy.rb to implement these rules.
  2. Implement a nonce generator in the initializer.
  3. Update your application.html.erb to include the csp_meta_tag.
  4. Apply the correct nonce attribute to an inline <script> tag in your layout.
  5. Ensure the policy is enforced (not in report-only mode).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.