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_policyblock 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_taghelper for exposing the nonce to JavaScript in your layout. config.content_security_policy_report_onlyto 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
SecureRandomper 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_uritakes 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.