Skip to content
MDView as Markdown

Ruby on Rails

Rails has shipped a CSP DSL for years, and it is one of the more pleasant implementations: the policy is Ruby rather than a string, nonces are wired in through a configurable generator, and report-only is a single boolean.

What Rails gives you

  • A content_security_policy block in an initializer, with a method per directive.
  • Per-controller overrides, so a policy can be tightened or relaxed on specific actions without duplicating the whole thing.
  • config.content_security_policy_nonce_generator, a lambda that receives the request and returns the nonce.
  • The csp_meta_tag helper for exposing the nonce to JavaScript in your layout.
  • config.content_security_policy_report_only to switch the whole policy to the report-only header.

Setting the header

Define the policy in config/initializers/content_security_policy.rb. Your URLs come from the Setup page:

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, "'report-sha256'"
  policy.report_uri  "https://abc123.report-uri.com/r/d/csp/reportOnly"
end
Rails.application.config.content_security_policy_report_only = true

The Rails CSP DSL has no report_to, so a small Rack middleware appends it to the header and adds the two Reporting API headers. It is inserted before the CSP middleware, which puts it outside it, so it sees the header on the way out. It uses the lower-case header names of Rack 3, so it needs Rails 7.1 or later:

# lib/reporting_api_headers.rb
class ReportingApiHeaders
  ENDPOINT = "https://abc123.report-uri.com/a/d/g"
  def initialize(app)
    @app = app
  end
  def call(env)
    status, headers, body = @app.call(env)
    csp = "content-security-policy-report-only"
    headers[csp] = "#{headers[csp]}; report-to default" if headers[csp]
    headers["reporting-endpoints"] = %(default="#{ENDPOINT}")
    headers["report-to"] = { group: "default", max_age: 31536000, endpoints: [{ url: ENDPOINT }], include_subdomains: true }.to_json
    [status, headers, body]
  end
end
# config/application.rb
require_relative "../lib/reporting_api_headers"
# ...
config.middleware.insert_before ActionDispatch::ContentSecurityPolicy::Middleware, ReportingApiHeaders

The Reporting-Endpoints and Report-To headers tell the browser where report-to default sends reports, including the CSP Integrity reports that 'report-sha256' turns on.

Flip content_security_policy_report_only to false when you are ready to enforce, and point report_uri at the enforce endpoint at the same time. The middleware then needs to read content-security-policy instead.

Adding a nonce

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

Add csp_meta_tag to your layout's <head>, and Rails will attach the nonce to tags generated by javascript_tag and friends.

Things to watch for

  • A nonce generator keyed off something stable - the session ID is the usual mistake - is not a nonce. Use SecureRandom per request.
  • Per-controller overrides replace rather than merge in some cases; check the resulting header rather than assuming, especially where an override sits on a parent controller.
  • report_uri takes the raw URL. It does not need quoting, unlike the source expressions.

Official documentation

Ruby on Rails - Securing Rails Applications: Content Security Policy

Start Monitoring with Report URI

Already using us for CSP? Find your CSP reporting directives on Setup, or view your CSP reports.

New to Report URI? Create an account and grab your CSP reporting directives from the Setup page.

Start your free trial