Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DSL for configuring Content-Security-Policy header #31162

Merged
merged 1 commit into from Nov 27, 2017
Merged

Conversation

@pixeltrix
Copy link
Member

@pixeltrix pixeltrix commented Nov 15, 2017

Pushing up to get feedback on implementation

Documentation
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy

Examples
Global config in an initializer:

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do
  p.default_src :self, :https
  p.font_src    :self, :https, :data
  p.img_src     :self, :https, :data
  p.object_src  :none
  p.script_src  :self, :https
  p.style_src   :self, :https, :unsafe_inline

  # Specify URI for violation reports
  # p.report_uri  "/csp-violation-report-endpoint"
end

# Report CSP violations to a specified URI
# For further information see the following documentation:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
# Rails.application.config.content_security_policy_report_only = true

Per controller configuration:

# Override policy inline
class PostsController < ApplicationController
  content_security_policy do |p|
    p.upgrade_insecure_requests true
  end
end

# Using literal values
class PostsController < ApplicationController
  content_security_policy do |p|
    p.base_uri "https://www.example.com"
  end
end

# Using mixed static and dynamic values
class PostsController < ApplicationController
  content_security_policy do |p|
    p.base_uri :self, -> { "https://#{current_user.domain}.example.com" }
  end
end
@pixeltrix pixeltrix force-pushed the add-csp-config branch to e70f128 Nov 15, 2017
Copy link
Member

@jeremy jeremy left a comment

Lovely. Thanks for digging in to this. Added comments on 1. protect_content method naming; 2. declaring the base content security policy rather than using a boolean flag to indicate whether to use :application as the default policy; and 3. separating policy definitions from policy enforcement decisions, moving report-only out to the controller.

Also, food for thought: should the base policy be applied to all HTML responses in a Rack middleware? Catching other mounted apps, static pages, error pages, etc.

self.content_security_policies = PolicyCollection.new

config_accessor :default_protect_content
self.default_protect_content = false

This comment has been minimized.

@jeremy

jeremy Nov 16, 2017
Member

Good spot to specify the app-wide policy. Since this is the base policy for the whole app, be definitive:

# Set the base policy
config.action_controller.content_security_policy = :application

# No app-wide policy
config.action_controller.content_security_policy = nil

This comment has been minimized.

@pixeltrix

pixeltrix Nov 16, 2017
Author Member

Agree with a single base policy so this becomes irrelevant.

@policies.fetch(name).clone
rescue KeyError
raise ArgumentError, "Unknown content security policy: #{name.inspect}"
end

This comment has been minimized.

@jeremy

jeremy Nov 16, 2017
Member

Dupes #fetch impl above.

This comment has been minimized.

@pixeltrix

pixeltrix Nov 16, 2017
Author Member

Irrelevant now that collection is going away

elsif values.first
@directives["sandbox"] = values
else
@directives.delete("sandbox")

This comment has been minimized.

@jeremy

jeremy Nov 16, 2017
Member

What would the caller for this case look like?

This comment has been minimized.

@pixeltrix

pixeltrix Nov 16, 2017
Author Member

It's if you need to disable the value on a controller basis, e.g.

class LegacyPagesController < ApplicationController
  content_security_policy do |p|
    p.sandbox false
  end
end
end

def report_only!(uri:)
@report_only = true

This comment has been minimized.

@jeremy

jeremy Nov 16, 2017
Member

Would treat the policy itself and how it's enforced as separate concerns. Can hoist this (and header_name) out to the controller and handle in set_content_security_policy.

directives.each do |directive, sources|
copy[directive] = sources.map(&:dup)
end
end

This comment has been minimized.

@jeremy

jeremy Nov 16, 2017
Member

directives.transform_values { |sources| sources.map(&:dup) }
MAPPINGS.fetch(source)
else
raise ArgumentError, "Unknown content security policy source mapping: #{source.inspect}"
end

This comment has been minimized.

@jeremy

jeremy Nov 16, 2017
Member

Can incorporate the existence check into the fetch:

MAPPINGS.fetch(source) do
  raise ArgumentError, "Unknown content security policy source mapping: #{source.inspect}"
end
elsif sources
directive
else
nil

This comment has been minimized.

@jeremy

jeremy Nov 16, 2017
Member

Appears that a directive sources will never be nil, should just be absent. Raise here?

This comment has been minimized.

@pixeltrix

pixeltrix Nov 16, 2017
Author Member

This is when a directive is set to false - there's a compact operation that removes these nil values in header_value:

def header_value(context = nil)
  build_directives(context).compact.join("; ") + ";"
end

unless policy.empty?
self.content_security_policy = policy
end

This comment has been minimized.

@jeremy

jeremy Nov 16, 2017
Member

Maybe an edge case, but it could be sensible to set the header even if the policy is empty, making it clear that we've set an empty policy rather than omitting it.

This comment has been minimized.

@pixeltrix

pixeltrix Nov 16, 2017
Author Member

Yes, agreed.

end
end

after_action :set_content_security_policy, unless: :content_security_policy_present?

This comment has been minimized.

@jeremy

jeremy Nov 16, 2017
Member

Do we foresee a controller action providing a custom policy that should preclude the declared controller policy for that action?

This comment has been minimized.

@pixeltrix

pixeltrix Nov 16, 2017
Author Member

The unless: :content_security_policy_present? was more about handling upgraded apps that may have set the header manually - didn't want to break them.

policy :report_only, using: :application do |p|
p.report_only!(uri: "/csp-violation-report-endpoint")
end
end

This comment has been minimized.

@jeremy

jeremy Nov 16, 2017
Member

Not sure about this policy collection API for central definition of multiple policies. I think in practice we'll have a single base policy that's extended in controller subclasses. If there's a set of controllers that use a stricter policy, I'd probably start by mixing in a concern that sets the strict policy.

This comment has been minimized.

@pixeltrix

pixeltrix Nov 16, 2017
Author Member

Agreed - a single base policy with overrides either in concern or a common parent controller class makes the implementation simpler and doesn't lose any flexibility.

@pixeltrix
Copy link
Member Author

@pixeltrix pixeltrix commented Nov 16, 2017

@jeremy thanks for the review - agree that we should generate the default policy for all HTML responses so will move the policy class out to Action Dispatch and then just have the override implementation in Action Controller.

@pixeltrix pixeltrix force-pushed the add-csp-config branch 2 times, most recently from 9404f54 to 727c415 Nov 17, 2017
@kaspth
kaspth approved these changes Nov 26, 2017
Copy link
Member

@kaspth kaspth left a comment

Minor nits, but overall on API 👍


def content_security_policy_report_only(*args)
options = args.extract_options!
flag = args.empty? ? true : args.first.present?

This comment has been minimized.

@kaspth

kaspth Nov 26, 2017
Member

Could this be simplified with some kwarg help?

def content_security_policy_report_only(report_only = true, **options)
end
if MAPPINGS.key?(source)
MAPPINGS.fetch(source)
else
raise ArgumentError, "Unknown content security policy source mapping: #{source.inspect}"

This comment has been minimized.

@kaspth

kaspth Nov 26, 2017
Member

Could do:

MAPPINGS.fetch(source) do
  raise ArgumentError, 
end
p.style_src :self, :https, :unsafe_inline

# Specify URI for violation reports
# p.report_uri "/csp-violation-report-endpoint"

This comment has been minimized.

@kaspth

kaspth Nov 26, 2017
Member

Extra space after p.report_uri.

# For further information see the following documentation
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy

Rails.application.config.content_security_policy do |p|

This comment has been minimized.

@kaspth

kaspth Nov 26, 2017
Member

Should this config be commented out for upgrading apps? Or should it be loose enough that apps can upgrade seamlessly and get more security?

This comment has been minimized.

@pixeltrix

pixeltrix Nov 27, 2017
Author Member

Do initializers get generated for upgrading apps automatically?

This comment has been minimized.

@kaspth

kaspth Nov 27, 2017
Member

IIRC, yes.

This comment has been minimized.

@pixeltrix

pixeltrix Nov 27, 2017
Author Member

Okay, will investigate what the preferred solution is to be once the beta ships and it's easier to test against upgrading real world apps.

@pixeltrix pixeltrix force-pushed the add-csp-config branch from 727c415 Nov 27, 2017
@pixeltrix pixeltrix changed the title [WIP] Add DSL for configuring Content-Security-Policy header Add DSL for configuring Content-Security-Policy header Nov 27, 2017
@pixeltrix pixeltrix force-pushed the add-csp-config branch to 456c3ff Nov 27, 2017
@pixeltrix pixeltrix merged commit afc2b42 into master Nov 27, 2017
3 checks passed
3 checks passed
codeclimate All good!
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
continuous-integration/travis-ci/push The Travis CI build passed
Details
@pixeltrix pixeltrix deleted the add-csp-config branch Nov 27, 2017
pixeltrix added a commit that referenced this pull request Nov 27, 2017
prathamesh-sonpatki added a commit to prathamesh-sonpatki/rails that referenced this pull request Nov 27, 2017
- Followup of rails#31162
prathamesh-sonpatki added a commit to prathamesh-sonpatki/rails that referenced this pull request Nov 27, 2017
pixeltrix added a commit that referenced this pull request Nov 27, 2017
Fix CHANGELOG for CSP PR #31162 [ci skip]
@dwightwatson
Copy link
Contributor

@dwightwatson dwightwatson commented Nov 27, 2017

Is there a guide or any documentation on how to use this in development? For example, JavaScripts served through the asset pipeline or Webpacker (either precompiled or with the hot-reloading development server) are all HTTP and therefore left broken with the default configuration.

@pixeltrix
Copy link
Member Author

@pixeltrix pixeltrix commented Nov 27, 2017

@dwightwatson I thought that 'self' would allow HTTP assets served from the same domain and https: allows third party assets from HTTPS sources? I took the defaults from #24961 which is described as 'lenient'.

@dwightwatson
Copy link
Contributor

@dwightwatson dwightwatson commented Nov 27, 2017

@pixeltrix That's what I thought as well, and after digging further into it that appears to be correct.

The issue I'm seeing is related but has to do with unsafe-eval. Looks like Webpacker bundles up modules using the eval method and this default CSP configuration doesn't allow for that.

Should I open another ticket to follow this issue instead? It might require the CSP configuration to be altered to allow for Webpacker assets or Webpacker might need to be configured to bundle another way.

Uncaught EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self' https:".

@pixeltrix
Copy link
Member Author

@pixeltrix pixeltrix commented Nov 27, 2017

@dwightwatson is it the webpacker gem that uses eval or webpack itself?

@pixeltrix
Copy link
Member Author

@pixeltrix pixeltrix commented Nov 27, 2017

@dwightwatson I wonder whether rails/webpacker#1011 is related? Can you change config.devtool to cheap-module-source-map and see whether that fixes your problem.

@dwightwatson
Copy link
Contributor

@dwightwatson dwightwatson commented Nov 27, 2017

I was just reading the same thing - yeah it looks like that configuration change fixes the issue, all working locally now. Is making that configuration the new default something that could be reconsidered?

@pixeltrix
Copy link
Member Author

@pixeltrix pixeltrix commented Nov 27, 2017

Is making that configuration the new default something that could be considered?

Possibly - will raise it in the Campfire channel.

@crispinheneise
Copy link

@crispinheneise crispinheneise commented Nov 28, 2017

I'm also having this issue, using rack-livereload:

Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' https:". Either the 'unsafe-inline' keyword, a hash ('sha256-rlZ58lcfumEQqoV6vyIbnvH6c7q0w93savs/e1DR0TA='), or a nonce ('nonce-...') is required to enable inline execution.

Refused to load the script 'http://localhost:35729/livereload.js?' because it violates the following Content Security Policy directive: "script-src 'self' https:".

@pixeltrix
Copy link
Member Author

@pixeltrix pixeltrix commented Nov 28, 2017

@crispinheneise the quick fix is to add :unsafe_inline to the p.script_src line in your content_security_policy.rb initializer, however you lose some of the security that the CSP gives you in preventing XSS attacks. Ideally the rack-livereload middleware would add a nonce to the script tags it injects and then modify the CSP header to add nonce-<randomvalue>. That would allow you to use live reload in development without affecting your production environment.

@crispinheneise
Copy link

@crispinheneise crispinheneise commented Nov 28, 2017

This may be an edge case, or it might belong to rack-livereload - if so please ignore.

In order to get CSP to work with rack-livereload I had to add unsafe-inline as @pixeltrix mentioned. Plus, I also had to add a script_src for port 35729 and a connect_src directive for the web socket. This is only needed in development, so this is what I did in content_security_policy.rb:

  if Rails.env.development?
    p.script_src  :self, :https, :unsafe_inline, 'http://localhost:35729'
    p.connect_src 'http://localhost:35729', 'ws://localhost:35729'
  else
    p.script_src  :self, :https
  end
@pixeltrix
Copy link
Member Author

@pixeltrix pixeltrix commented Nov 28, 2017

@crispinheneise only issue is that change is that you'll only find out about your own app's use of inline scripts when you deploy to production 😟

claudiob added a commit to claudiob/rails that referenced this pull request Jan 30, 2018
Document in the guides the new middleware added in rails#31162
[ci skip]
@nsingh
Copy link

@nsingh nsingh commented Jun 14, 2018

Hi @pixeltrix ,

Thanks for implementing CSP in Rails! We are still on Rails 5.0.6 so we had to backport your code to get things in motion.

I did have one question regarding the ability to set both headers:

POLICY = "Content-Security-Policy".freeze
POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze

Currently it seems like if Report-Only option is true, it takes precedence:-

def header_name(request)
  if request.content_security_policy_report_only
    POLICY_REPORT_ONLY
  else
    POLICY
  end
end

The CSP documentation says that both headers can be present in the same response and could have different policies.

I am wondering if this was deliberate and whether Rails has plans to honor both headers in the near future?

jacobbednarz added a commit to jacobbednarz/rails that referenced this pull request Jul 26, 2018
A HTTP feature policy is Yet Another HTTP header for instructing the
browser about which features the application intends to make use of and
to lock down access to others. This is a new security mechanism that
ensures that should an application become compromised or a third party
attempts an unexpected action, the browser will override it and maintain
the intended UX.

WICG specification: https://wicg.github.io/feature-policy/

The end result is a HTTP header that looks like the following:

```
Feature-Policy: geolocation 'none'; autoplay https://example.com
```

This will prevent the browser from using geolocation and only allow
autoplay on `https://example.com`. Full feature list can be found over
in the WICG repository[1].

As of today Chrome and Safari have public support[2] for this
functionality with Firefox working on support[3] and Edge still pending
acceptance of the suggestion[4].

#### Examples

Using an initializer

```rb
# config/initializers/feature_policy.rb
Rails.application.config.feature_policy do |f|
  f.geolocation :none
  f.camera      :none
  f.payment     "https://secure.example.com"
  f.fullscreen  :self
end
```

In a controller

```rb
class SampleController < ApplicationController
  def index
    feature_policy do |f|
      f.geolocation "https://example.com"
    end
  end
end
```

Some of you might realise that the HTTP feature policy looks pretty
close to that of a Content Security Policy; and you're right. So much so
that I used the Content Security Policy DSL from rails#31162 as the starting
point for this change.

This change *doesn't* introduce support for defining a feature policy on
an iframe and this has been intentionally done to split the HTTP header
and the HTML element (`iframe`) support. If this is successful, I'll
look to add that on it's own.

Full documentation on HTTP feature policies can be found at
https://wicg.github.io/feature-policy/. Google have also published[5] a
great in-depth write up of this functionality.

[1]: https://github.com/WICG/feature-policy/blob/master/features.md
[2]: https://www.chromestatus.com/feature/5694225681219584
[3]: https://bugzilla.mozilla.org/show_bug.cgi?id=1390801
[4]: https://wpdev.uservoice.com/forums/257854-microsoft-edge-developer/suggestions/33507907-support-feature-policy
[5]: https://developers.google.com/web/updates/2018/06/feature-policy
jeremy added a commit that referenced this pull request Jul 10, 2019
A HTTP feature policy is Yet Another HTTP header for instructing the
browser about which features the application intends to make use of and
to lock down access to others. This is a new security mechanism that
ensures that should an application become compromised or a third party
attempts an unexpected action, the browser will override it and maintain
the intended UX.

WICG specification: https://wicg.github.io/feature-policy/

The end result is a HTTP header that looks like the following:

```
Feature-Policy: geolocation 'none'; autoplay https://example.com
```

This will prevent the browser from using geolocation and only allow
autoplay on `https://example.com`. Full feature list can be found over
in the WICG repository[1].

As of today Chrome and Safari have public support[2] for this
functionality with Firefox working on support[3] and Edge still pending
acceptance of the suggestion[4].

#### Examples

Using an initializer

```rb
# config/initializers/feature_policy.rb
Rails.application.config.feature_policy do |f|
  f.geolocation :none
  f.camera      :none
  f.payment     "https://secure.example.com"
  f.fullscreen  :self
end
```

In a controller

```rb
class SampleController < ApplicationController
  def index
    feature_policy do |f|
      f.geolocation "https://example.com"
    end
  end
end
```

Some of you might realise that the HTTP feature policy looks pretty
close to that of a Content Security Policy; and you're right. So much so
that I used the Content Security Policy DSL from #31162 as the starting
point for this change.

This change *doesn't* introduce support for defining a feature policy on
an iframe and this has been intentionally done to split the HTTP header
and the HTML element (`iframe`) support. If this is successful, I'll
look to add that on it's own.

Full documentation on HTTP feature policies can be found at
https://wicg.github.io/feature-policy/. Google have also published[5] a
great in-depth write up of this functionality.

[1]: https://github.com/WICG/feature-policy/blob/master/features.md
[2]: https://www.chromestatus.com/feature/5694225681219584
[3]: https://bugzilla.mozilla.org/show_bug.cgi?id=1390801
[4]: https://wpdev.uservoice.com/forums/257854-microsoft-edge-developer/suggestions/33507907-support-feature-policy
[5]: https://developers.google.com/web/updates/2018/06/feature-policy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

6 participants
You can’t perform that action at this time.