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

Published November 29, 2015


You might also enjoy

Anonymizing IPs in Ruby

Scaling Reads

Google OAuth with Devise


All code examples are public domain.
Use them however you’d like (licensed under CC0).