Grouping Content Security Policy configs

The content_security_policy config can become unwieldy when supporting multiple 3rd party services. For example, the Intercom CSP v2 config is quite extensive.

I’m currently monkey-patching ActionDispatch::ContentSecurityPolicy to add an append method allowing me to merge CSP directives.

Rails.application.config.content_security_policy do |policy|
  policy.script_src :self
  # ...

  # Youtube
  policy.append do |youtube_policy|
    youtube_policy.script_src "www.youtube.com"
    # ...
  end

  # Google Analytics
  policy.append do |ga_policy|
    ga_policy.script_src "www.google-analytics.com"
    # ...
  end

  # Intercom
  # https://www.intercom.com/help/en/articles/3894-using-intercom-with-content-security-policy
  policy.append do |intercom_policy|
    intercom_policy.script_src "*.intercom.io", "*.intercomcdn.com"
    # ...
  end
end

It also makes it easier to make conditional directives.

if Rails.env.production?
  # CDN
  policy.append do |cdn_policy|
    cdn_policy.script_src ENV["RAILS_ASSET_HOST"]
  end
end

Here’s the monkey patch.

class ActionDispatch::ContentSecurityPolicy
  def append(&block)
    sub_policy = ActionDispatch::ContentSecurityPolicy.new(&block)
    sub_policy.directives.each do |key, sources|
      if @directives[key]
        @directives[key] = @directives[key].concat(sources).uniq
      else
        @directives[key] = sources
      end
    end
  end
end

Is this something that would fit in Rails core? If so I can turn it into a PR.

6 Likes

Securing Rails Applications — Ruby on Rails Guides can solve your problem? I think around_action callback can solve to all of actions and dynamic controll.

From my understanding that is only for overriding the content security policy on a per-controller or action basis.

What I’m proposing is a way to organize the global content security policy. It does not change the final result, it is just for internal organization.

1 Like

My understanding is same as your understanding. inheritance or including module will solve all of controllers.

however I think it is good to provide option as well.

Quite neat, I would do:

policy.append do |p|
  p.script_src "https://example.com"
end

append would be useful for directives which are only needed in certain places.

Say your app generally needs to access scripts from self and a CDN. No problem:

Rails.application.config.content_security_policy do |policy|
  policy.script_src :self, 'https://cdn.company.com'
end

Now say that on the registration page, you also want Recaptcha. The current API gives you two ways to solve this.

Current solution 1

You could add it to your app-wide policy:

Rails.application.config.content_security_policy do |policy|
  policy.script_src :self,
                    'https://cdn.company.com',
                    'https://google.com'
end

But then it’s not exactly clear why it’s there. A few more cases like this, and your app-wide CSP becomes a forest. Also, this adds lots of unnecessary directives to most responses. For some directives, like blob:, this could increase security risk.

Current solution 2

Permit Recaptcha in the controller:

class RegistrationsController < ApplicationController
  content_security_policy do |policy|
    policy.script_src :self, 'https://cdn.company.com', 'https://google.com'
  end
end

But then RegistrationsController needs to know that some scripts come from the CDN’s URL and some come from self. If that ever changes, you need to find and update all instances of content_security_policy in your app.

Better solution with append

With append, though, there’s no problem. You can set just the directives which are relevant to the controller right where you need them:

class RegistrationsController < ApplicationController
  content_security_policy do |policy|
    policy.script_src << 'https://google.com'
  end
end

This gives you the best of both words. The app policy doesn’t need to know about local concerns, and the controller doesn’t need to know about app-wide concerns.

Note: it’s not trivial to design the actual API. How should append work when the base is :none? I’m sure there are lots of other things to think about, too.