ruby

How to Implement a Robust Content Security Policy in Your Rails Application

Learn how to implement Content Security Policy in Rails with nonces, hashes, and violation reporting. Protect your app from injection attacks today.

How to Implement a Robust Content Security Policy in Your Rails Application

Let’s talk about keeping your Rails application safe. Imagine your website is a house. You have furniture, pictures on the walls, and maybe a safe. A Content Security Policy, or CSP, is like a detailed security system for that house. Instead of just locking the front door, it specifies exactly who is allowed to bring in furniture, hang pictures, or even enter certain rooms. In web terms, it tells the browser precisely which sources are permitted to load scripts, styles, images, or other resources. This stops a very common type of attack where malicious code is injected into your site.

I think of it as moving from a simple “lock the door” approach to having a full security checklist for every item that comes in. The core mechanism is an HTTP header. Your Rails app sends this header with every response, and the browser enforces the rules you set.

Here is the most straightforward way to start. You create an initializer file in your Rails project.

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
  policy.default_src :self, :https
  policy.font_src    :self, :https, :data
  policy.img_src     :self, :https, :data
  policy.object_src  :none
  policy.script_src  :self, :https
  policy.style_src   :self, :https
end

Let me explain what this does. The default_src rule is a fallback for any resource type you don’t specify. :self means the current origin—your own domain. :https means any HTTPS source. So, by default, we allow resources from our own site and secure external sources. We then define specific rules. font_src and img_src also allow :data, which is necessary for inline data URLs often used for icons or small images. The line policy.object_src :none is crucial. It completely disallows plugins like Flash, which are common attack vectors.

You might notice a problem. Many Rails applications have inline JavaScript and CSS, especially in views. With the policy above, those inline blocks would be blocked. This is a good thing security-wise, but it breaks the app. We have two main solutions: using a nonce or a hash.

A nonce is a “number used once.” It’s a random, cryptographically secure string that changes with every page request. You add it to your CSP header and also to your inline tags. The browser will only execute the inline script if the nonce in the tag matches the one in the header. Rails has built-in support for this.

First, you enable nonce generation.

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy_nonce_generator = ->(request) { SecureRandom.base64(16) }
Rails.application.config.content_security_policy_nonce_directives = %w(script-src style-src)

This tells Rails to generate a new nonce for each request and apply it to the script-src and style-src directives. Now, you need to add this nonce to your inline code. In your layout file, it’s automatic for javascript_include_tag and stylesheet_link_tag. For true inline blocks, you use helpers.

<%# In your view template %>
<%= content_tag :script, nonce: true do %>
  console.log('This inline script is allowed because it has a valid nonce.');
<% end %>

If you look at the page source, you’ll see something like <script nonce="abc123...">. The header will include script-src 'self' 'nonce-abc123...'. They match, so the script runs.

For development, you might want a more forgiving policy to avoid constant blocking while you work. You can use a report-only mode or adjust the policy.

Rails.application.config.content_security_policy do |policy|
  # ... other directives ...
  if Rails.env.development?
    policy.script_src :self, :https, :unsafe_eval, :unsafe_inline
    policy.style_src  :self, :https, :unsafe_inline
  else
    policy.script_src :self, :https
    policy.style_src  :self, :https
  end
end

Notice the :unsafe_inline and :unsafe_eval sources. These are, as the name suggests, not safe for production. They effectively bypass CSP for inline scripts and eval() functions. Use them only in development as a stepping stone. A better approach for development is to keep your strict policy but set up a violation reporting endpoint.

This is a critical part of the process. You don’t want to deploy a CSP and just hope it works. You need to see what it’s blocking. You can tell the browser to send reports to a URL you control.

Rails.application.config.content_security_policy do |policy|
  # ... other directives ...
  policy.report_uri "/csp_reports"
end

# In report-only mode for testing (doesn't block, just reports)
Rails.application.config.content_security_policy_report_only = true

Now, create a controller to handle these reports.

# app/controllers/csp_reports_controller.rb
class CspReportsController < ApplicationController
  # CSP reports come without the normal authenticity token.
  skip_before_action :verify_authenticity_token

  def create
    report = JSON.parse(request.body.read)['csp-report']
    Rails.logger.warn "CSP Violation: #{report.to_json}"

    # You can store this in your database for analysis
    # CspViolation.create!(
    #   document_uri: report['document-uri'],
    #   violated_directive: report['violated-directive'],
    #   blocked_uri: report['blocked-uri']
    # )

    head :ok
  end
end

And add a route for it.

# config/routes.rb
post '/csp_reports', to: 'csp_reports#create'

With this in place, you can deploy your application with report_only set to true. Your browser will log violations to your Rails log or database without actually blocking anything. This gives you a complete list of everything you need to fix: inline scripts, styles loaded from third-party CDNs, tracking pixels, etc.

Now let’s address external resources. A modern app uses fonts from Google, payment scripts from Stripe, analytics from somewhere else. You must explicitly allow these in your CSP. Let’s build a cleaner way to manage these sources.

# app/lib/csp/sources.rb
module Csp
  module Sources
    GOOGLE_FONTS   = 'https://fonts.googleapis.com'.freeze
    GOOGLE_STATIC  = 'https://fonts.gstatic.com'.freeze
    STRIPE_JS      = 'https://js.stripe.com'.freeze
    STRIPE_API     = 'https://api.stripe.com'.freeze
    GOOGLE_ANALYTICS = 'https://www.google-analytics.com'.freeze
    GRAVATAR       = 'https://secure.gravatar.com'.freeze

    # A method to return the right sources per environment or feature
    def self.for_environment(env)
      base = [ :self, :https ]
      case env
      when 'production'
        base + [ GOOGLE_FONTS, GOOGLE_STATIC, STRIPE_JS, GRAVATAR ]
      when 'development'
        base + [ GOOGLE_FONTS, GOOGLE_STATIC, :unsafe_eval, :unsafe_inline ]
      else
        base
      end
    end
  end
end

Then, in your initializer, you can use this structured list.

Rails.application.config.content_security_policy do |policy|
  env_sources = Csp::Sources.for_environment(Rails.env)

  policy.default_src :self, :https
  policy.font_src    :self, :https, :data, Csp::Sources::GOOGLE_STATIC
  policy.img_src     :self, :https, :data, Csp::Sources::GRAVATAR
  policy.script_src  *env_sources
  policy.style_src   :self, :https, Csp::Sources::GOOGLE_FONTS, "'unsafe-inline'"
  policy.connect_src :self, Csp::Sources::STRIPE_API
  policy.frame_src   :self, Csp::Sources::STRIPE_JS
end

The connect_src directive is important. It controls which URLs your JavaScript can connect to using fetch, XMLHttpRequest, or WebSockets. If you use Action Cable or live updates, you need to add your WebSocket URL here. frame_src controls where you can embed <frame>, <iframe>, or <embed> tags.

Sometimes you need a dynamic policy that changes based on the page or user. For example, an admin panel might need different rules. You can achieve this with a custom Rack middleware.

# app/middleware/dynamic_csp_middleware.rb
class DynamicCspMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    request = ActionDispatch::Request.new(env)
    status, headers, response = @app.call(env)

    # Build a policy based on the request
    csp_header = build_csp_for(request)
    headers['Content-Security-Policy'] = csp_header unless csp_header.nil?

    [status, headers, response]
  end

  private

  def build_csp_for(request)
    policy = ActionDispatch::ContentSecurityPolicy.new

    # Base policy
    policy.default_src :self, :https
    policy.script_src  :self, :https

    # Add Stripe on checkout pages
    if request.path.include?('/checkout')
      policy.script_src  :self, :https, 'https://js.stripe.com'
      policy.frame_src   'https://js.stripe.com'
    end

    # Allow a specific inline script for this page using a hash
    if request.path == '/special-widget'
      # The hash is the SHA256 of the exact script content
      policy.script_src :self, :https, "'sha256-abc123...'"
    end

    policy.build
  end
end

You need to register this middleware.

# config/application.rb
module YourApp
  class Application < Rails::Application
    config.middleware.use DynamicCspMiddleware
  end
end

This gives you immense flexibility. The hash method I used for the ‘/special-widget’ page is an alternative to nonces. You calculate the SHA256 hash of the entire inline script block and add it to the script-src directive. The browser will compute the hash of the script and execute it only if they match. It’s less flexible than a nonce because the script content can never change without updating the hash, but it’s useful for static, critical scripts.

Managing all of this can become complex. I find it helpful to create a simple dashboard to see violations.

# app/controllers/admin/csp_dashboard_controller.rb
module Admin
  class CspDashboardController < ApplicationController
    before_action :authorize_admin

    def index
      # Assuming you stored violations in a CspViolation model
      @recent_violations = CspViolation.order(created_at: :desc).limit(50)
      @summary = {
        last_24_hours: CspViolation.where('created_at > ?', 24.hours.ago).count,
        top_blocked_uri: CspViolation.group(:blocked_uri).order('count_all desc').limit(5).count,
        top_directive: CspViolation.group(:violated_directive).order('count_all desc').limit(5).count
      }
    end
  end
end

This dashboard helps you see patterns. Maybe you’re constantly blocking a useful browser extension your team uses, or maybe you’re seeing probing attacks from specific sources.

Finally, let’s talk about testing. You should not deploy a CSP without tests. Here’s how you can write a system test to ensure your policy is active and working.

# test/system/csp_test.rb
require 'application_system_test_case'

class CspTest < ApplicationSystemTestCase
  test 'CSP header is present' do
    visit root_url
    csp_header = page.response_headers['Content-Security-Policy']

    assert csp_header.present?, 'CSP header is missing'
    assert_includes csp_header, 'script-src', 'CSP header does not contain script-src directive'
  end

  test 'inline script is blocked without nonce' do
    # This test requires a page with a known, nonced script.
    visit home_url
    script_tags = page.all('script', visible: false)

    script_tags.each do |script|
      # Every script tag should have a nonce attribute
      assert script[:nonce].present?, 'Found a script tag without a nonce attribute'
    end
  end
end

You can also use tools like the secure_headers gem, which provides more advanced CSP features and easier management, but understanding the manual setup gives you a firm grasp of the principles.

The journey to a full CSP is incremental. Start with report-only mode. Analyze the logs. Fix the issues one by one, either by adding external sources to your policy, moving inline code to external files, or adding nonces. Then, switch from report-only to enforcing. Continue to monitor the reports. It’s a powerful layer of security that directly tells the browser what your application’s rules are, making it exponentially harder for attackers to inject malicious content. It’s not just a configuration; it’s a fundamental shift in how your application defines its trust boundaries.

Keywords: Rails CSP, Content Security Policy Rails, Rails application security, Ruby on Rails security headers, CSP implementation Rails, Rails XSS prevention, Rails HTTP security headers, Content Security Policy tutorial, Rails CSP nonce, Rails initializer security, CSP report-only mode Rails, Rails CSP violations, Rails script-src directive, Rails style-src directive, Rails inline script security, CSP nonce generator Rails, Rails SecureRandom nonce, Rails CSP middleware, dynamic CSP Rails, Rails CSP hash method, SHA256 CSP hash Rails, Rails CSP violation reporting, Rails csp_reports controller, Rails CSP testing, Rails secure headers gem, Rails CSP external sources, Google Fonts CSP Rails, Stripe CSP Rails, Rails WebSocket CSP, connect-src Rails, frame-src Rails, Rails CSP dashboard, Rails object-src none, CSP default-src Rails, Rails content security policy best practices, Rails CSP production configuration, Rails CSP development configuration, Rails XSS attack prevention, Rails browser security policy, Rails CSP unsafe-inline, Rails CSP unsafe-eval, Rails action cable CSP, Rails CSP enforcement, incremental CSP deployment Rails, Rails security configuration, web application security Rails, Rails CSP report URI, Rails CSP admin dashboard, Rails CSP violation monitoring, Rails security initializer



Similar Posts
Blog Image
Rails Caching Strategies: Performance Optimization Guide with Code Examples (2024)

Learn essential Ruby on Rails caching strategies to boost application performance. Discover code examples for fragment caching, query optimization, and multi-level cache architecture. Enhance your app today!

Blog Image
**Essential Ruby Performance Monitoring Tools Every Developer Should Master in 2024**

Optimize Ruby app performance with essential monitoring tools: memory_profiler, stackprof, rack-mini-profiler & more. Learn profiling techniques to boost speed & efficiency.

Blog Image
Ruby's Ractor: Supercharge Your Code with True Parallel Processing

Ractor in Ruby 3.0 brings true parallelism, breaking free from the Global Interpreter Lock. It allows efficient use of CPU cores, improving performance in data processing and web applications. Ractors communicate through message passing, preventing shared mutable state issues. While powerful, Ractors require careful design and error handling. They enable new architectures and distributed systems in Ruby.

Blog Image
9 Essential Ruby Gems for Rails Database Migrations: A Developer's Guide

Discover 9 essential Ruby gems for safe, efficient Rails database migrations. Learn best practices for zero-downtime schema changes, performance monitoring, and data transformation without production issues. Improve your migration workflow today.

Blog Image
10 Proven Strategies to Boost Rails View Performance

Optimize Rails view rendering: Learn caching, helpers, and performance tips to speed up your web apps. Boost efficiency now!

Blog Image
Is Redis the Secret Sauce Missing from Your Rails App?

Mastering Redis: Boost Your Rails App’s Performance from Caching to Background Jobs