Adding CSP to Rails
Content Security Policy can be an effective way to prevent XSS attacks. If you aren’t familiar, here’s a great intro.
To get started with Rails, first add the header to all requests in your ApplicationController
. We want to start by blocking content in development so we notice it, but only report it in production so nothing breaks.
before_action :set_csp
# use constants and freeze for performance
CSP_HEADER_NAME = (Rails.env.production? ? "Content-Security-Policy-Report-Only" : "Content-Security-Policy").freeze
CSP_HEADER_VALUE = "default-src *; report-uri /csp_reports?report_only=#{CSP_HEADER_NAME.include?("Report-Only")}".freeze
def set_csp
response.headers[CSP_HEADER_NAME] = CSP_HEADER_VALUE
end
Reports
Create a model to track reports.
rails g model CspReport
And in the migration, do:
class CreateCspReports < ActiveRecord::Migration
def change
create_table :csp_reports do |t|
t.text :document_uri
t.text :referrer
t.text :violated_directive
t.text :effective_directive
t.text :original_policy
t.text :blocked_uri
t.integer :status_code
t.text :user_agent
t.boolean :report_only
t.datetime :created_at
end
end
end
Add a controller to create the reports.
class CspReportsController < ApplicationController
skip_before_action :verify_authenticity_token
def create
report = JSON.parse(request.body.read)["csp-report"]
CspReport.create!(
document_uri: report["document-uri"],
referrer: report["referrer"],
violated_directive: report["violated-directive"],
effective_directive: report["effective-directive"],
original_policy: report["original-policy"],
blocked_uri: report["blocked-uri"],
status_code: report["status-code"],
user_agent: request.user_agent,
report_only: params[:report_only] == "true"
)
head :ok
end
end
Don’t forget the route.
resources :csp_reports, only: [:create]
Enforcing the Policy
Once the reports stop, you’ll want to enforce the policy in production.
CSP_HEADER_NAME = "Content-Security-Policy".freeze
Testing New Policies
You can have both an enforced policy and a report only policy, so use this to your advantage when changing policies. Make the new policy report only for a bit before enforcing it.
before_action :set_csp_report_only
# use constants and freeze for performance
CSP_REPORT_ONLY_HEADER_NAME = "Content-Security-Policy-Report-Only".freeze
CSP_REPORT_ONLY_HEADER_VALUE = "default-src https:; report-uri /csp_reports?report_only=true".freeze
def set_csp_report_only
response.headers[CSP_REPORT_ONLY_HEADER_NAME] = CSP_REPORT_ONLY_HEADER_VALUE
end