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

Setting two CSP headers, abstract out dynamic pieces #281

Merged
merged 32 commits into from
Sep 9, 2016

Conversation

oreoshake
Copy link
Contributor

@oreoshake oreoshake commented Aug 9, 2016

Fixes #256, #281

  • @modified bit strategy isn't working perfectly, I'm seeing objects being created when policies are not modified.
  • Has tests
  • Documentation updated

Setting two headers has been an often requested feature that has always had a workaround but was never a first class feature. This PR allows setting both headers.

In the process of supporting both, I decided to move away from hash-backed configs and towards class-based configs - mostly because it made debugging things very hard and mistyping a hash key is far to easy. This has the added benefit of not having to perform "deep clones" all the time and relying on hash voodoo. This is much clearer even if some of the operations are just glorified hash operations.

This also adds a @modified attribute which signals whether the cached headers can be used or not. This removes the need to track a dynamic_csp, choose between two configs, and use the idempotent_operations call which was all just a bit too funky and created the need for a lot of boilerplate code.

When writing support for override_content_security_policy_directives/append_content_security_policy_directives, I realized that the default policy is not very user-friendly and insecure at the same time. Updating the default policy should make it easier for people to use the default setting and make their policy dynamic as needed. The new policy is less restrictive for the most part, with the exception of object-src 'none' which is just an unsafe default.

The reason the DynamicConfig stuff is extracted into a module with some hackery is so that it can be reused when implementing FeaturePolicy which will benefit from the dynamic helper functions and non-hash-backed data. I like the idea, but the implementation looks a bit janky. I'll look into how ActiveRecord handles it's changed? function. I'm also questioning the use of update_directive/directive_value.

@coveralls
Copy link

coveralls commented Aug 9, 2016

Coverage Status

Coverage decreased (-0.4%) to 97.254% when pulling d19a551 on dynamic-csp-config-abstraction into 023008d on master.

@jacobbednarz
Copy link
Contributor

Where would you put this in terms of completion? Would you be confident in trying this out on an existing implementation yet? (with the exception of the @modified issue you mentioned above)

@oreoshake
Copy link
Contributor Author

I wouldn't use this implementation yet. The @modified bug may uncover other things. I'll have time to finish this out this week though.

@jacobbednarz
Copy link
Contributor

All good 👍 Just checking whether I should run this against our internal test suite and see if it pops up with anything but I'll hold off until the @modified stuff is tackled.

@@ -121,7 +119,8 @@ def deep_copy_if_hash(value)
def initialize(&block)
self.hpkp = OPT_OUT
self.referrer_policy = OPT_OUT
self.csp = self.class.send(:deep_copy, CSP::DEFAULT_CONFIG)
self.csp = ContentSecurityPolicyConfig::DEFAULT
Copy link
Contributor

@jacobbednarz jacobbednarz Aug 15, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would smarter defaults be to swap these conditions and allow a report only by default?

self.csp = OPT_OUT
self.csp_report_only = ContentSecurityPolicyConfig::DEFAULT

Or would this break backwards compatibility with existing implementations?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well moving to report only shouldn't break existing implementations, but the previous default was an enforced policy. The change to enforce-by-default was made at the last major version bump iirc so there's no reason to go "backwards" here. This PR does change the default policy to something more realistic and secure at the same time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds perfectly sane :)

@oreoshake oreoshake changed the title [WIP] Setting two CSP headers, abstract out dynamic pieces Setting two CSP headers, abstract out dynamic pieces Aug 17, 2016
@oreoshake oreoshake changed the title Setting two CSP headers, abstract out dynamic pieces [WIP] Setting two CSP headers, abstract out dynamic pieces Aug 17, 2016
@coveralls
Copy link

coveralls commented Aug 17, 2016

Coverage Status

Coverage decreased (-0.4%) to 97.257% when pulling c7a57f3 on dynamic-csp-config-abstraction into 023008d on master.

@oreoshake oreoshake changed the title [WIP] Setting two CSP headers, abstract out dynamic pieces Setting two CSP headers, abstract out dynamic pieces Aug 17, 2016
@oreoshake
Copy link
Contributor Author

I removed the WIP label. I still need to update the docs and take another glance for quality over the change, but this should be ready for use.

@jacobbednarz
Copy link
Contributor

Awesome! I'll pull this locally and run it through it's paces in our setup. :)

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.1%) to 97.571% when pulling 7600b90 on dynamic-csp-config-abstraction into 023008d on master.

4 similar comments
@coveralls
Copy link

Coverage Status

Coverage decreased (-0.1%) to 97.571% when pulling 7600b90 on dynamic-csp-config-abstraction into 023008d on master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.1%) to 97.571% when pulling 7600b90 on dynamic-csp-config-abstraction into 023008d on master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.1%) to 97.571% when pulling 7600b90 on dynamic-csp-config-abstraction into 023008d on master.

@coveralls
Copy link

coveralls commented Aug 18, 2016

Coverage Status

Coverage decreased (-0.1%) to 97.571% when pulling 7600b90 on dynamic-csp-config-abstraction into 023008d on master.

@jacobbednarz
Copy link
Contributor

Ah, makes sense. So if one was looking to only use a report only policy, it should be something like this?

SecureHeaders::Configuration.default do |config|
  config.csp = SecureHeaders::OPT_OUT
  config.csp_report_only = {
    report_uri: %w(https://reporting-endpoint),
    # .. snip
  }
end

@oreoshake
Copy link
Contributor Author

Correct. It doesn't feel right, however. Maybe adding smarts around detecting intention won't be so bad.

@jacobbednarz
Copy link
Contributor

Correct. It doesn't feel right, however.

Seems like a reasonable pattern to follow seeing that is how all the other directives are removed/ignored. Perhaps a sensible default of SecureHeaders::OPT_OUT is in order to ensure that if it's not explicitly set, you only get a read only policy?

Using the following to have a report only policy results in wrong number of arguments (2 for 0..1) (ArgumentError)

SecureHeaders::Configuration.default do |config|
  config.csp = SecureHeaders::OPT_OUT
  config.csp_report_only = {
    report_uri: %w(https://reporting-endpoint),
    # .. snip
  }
end

@oreoshake
Copy link
Contributor Author

Perhaps a sensible default of SecureHeaders::OPT_OUT is in order to ensure that if it's not explicitly set, you only get a read only policy?

Are you saying to make csp/csp_report_only default to OPT_OUT or are you saying something like:

if you set csp_report_only and csp == DEFAULT at that time, then opt out of csp and set csp_report_only.

The latter option was what I was going to try.

@oreoshake
Copy link
Contributor Author

Using the following to have a report only policy results in wrong number of arguments (2 for 0..1) (ArgumentError)

SecureHeaders::Configuration.default do |config|
  config.csp = SecureHeaders::OPT_OUT
  config.csp_report_only = {
    report_uri: %w(https://reporting-endpoint),
    # .. snip
  }
end

@jacobbednarz can you provide the full stacktrace/config? I just plopped that in and it worked (but needed a default-src to be set). Certainly no ArgumentErrors for me.

@jacobbednarz
Copy link
Contributor

Gemfile (running at 7600b90)

gem 'secure_headers', git: 'https://github.com/twitter/secureheaders.git',      
                      ref: 'dynamic-csp-config-abstraction'

Config (some of these directive values have been changed but it still results in the same error).

SecureHeaders::Configuration.default do |config|
  config.csp = SecureHeaders::OPT_OUT
  config.csp_report_only = {
    report_uri: %w(https://my-report-only-uri),
    preserve_schemes: true,

    default_src: %w(* data:),
    img_src: %w(* data:),
    style_src: %w(* 'self' 'unsafe-inline'),
    font_src: %w('self' data:),
    script_src: %w(
      *
      data:
      'self'
      'unsafe-eval'
      'unsafe-inline'
    ),
    connect_src: %w(*),
    base_uri: %w(*),
  }
end

Stacktrace

/opt/boxen/rbenv/versions/2.2.5/lib/ruby/gems/2.2.0/bundler/gems/secureheaders-7600b900c666/lib/secure_headers.rb:25:in `boom': wrong number of arguments (2 for 0..1) (ArgumentError)
    from /Users/Jacob/src/marketplace/config/initializers/http_security_headers.rb:40:in `block in <top (required)>'
    from /opt/boxen/rbenv/versions/2.2.5/lib/ruby/gems/2.2.0/bundler/gems/secureheaders-7600b900c666/lib/secure_headers/configuration.rb:34:in `instance_eval'
    from /opt/boxen/rbenv/versions/2.2.5/lib/ruby/gems/2.2.0/bundler/gems/secureheaders-7600b900c666/lib/secure_headers/configuration.rb:34:in `override'
    from /Users/Jacob/src/marketplace/config/initializers/http_security_headers.rb:39:in `<top (required)>'
    from /opt/boxen/rbenv/versions/2.2.5/lib/ruby/gems/2.2.0/gems/activesupport-3.2.22.4/lib/active_support/dependencies.rb:245:in `load'
    from /opt/boxen/rbenv/versions/2.2.5/lib/ruby/gems/2.2.0/gems/activesupport-3.2.22.4/lib/active_support/dependencies.rb:245:in `block in load'
    from /opt/boxen/rbenv/versions/2.2.5/lib/ruby/gems/2.2.0/gems/activesupport-3.2.22.4/lib/active_support/dependencies.rb:236:in `load_dependency'
    from /opt/boxen/rbenv/versions/2.2.5/lib/ruby/gems/2.2.0/gems/activesupport-3.2.22.4/lib/active_support/dependencies.rb:245:in `load'
    from /opt/boxen/rbenv/versions/2.2.5/lib/ruby/gems/2.2.0/gems/railties-3.2.22.4/lib/rails/engine.rb:593:in `block (2 levels) in <class:Engine>'
    from /opt/boxen/rbenv/versions/2.2.5/lib/ruby/gems/2.2.0/gems/railties-3.2.22.4/lib/rails/engine.rb:592:in `each'

@jacobbednarz
Copy link
Contributor

I agree with

if you set csp_report_only and csp == DEFAULT at that time, then opt out of csp and set csp_report_only.

as it makes the most sense to me.

@oreoshake
Copy link
Contributor Author

🆒 added that in 3225ed6 and I believe I fixed your error too. I don't have an explanation, however.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.1%) to 97.557% when pulling 3225ed6 on dynamic-csp-config-abstraction into 023008d on master.

3 similar comments
@coveralls
Copy link

Coverage Status

Coverage decreased (-0.1%) to 97.557% when pulling 3225ed6 on dynamic-csp-config-abstraction into 023008d on master.

@coveralls
Copy link

coveralls commented Aug 18, 2016

Coverage Status

Coverage decreased (-0.1%) to 97.557% when pulling 3225ed6 on dynamic-csp-config-abstraction into 023008d on master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.1%) to 97.557% when pulling 3225ed6 on dynamic-csp-config-abstraction into 023008d on master.

@jacobbednarz
Copy link
Contributor

My apologies, it was never the default csp/csp_report_only causing the issues - it is actually one of our overrides that was doing it. I still had config.csp in one of our overrides despite having it set to SecureHeaders::OPT_OUT 😞

@jacobbednarz
Copy link
Contributor

This is now passing our internal test suite too! Very nice effort ✨

@oreoshake oreoshake mentioned this pull request Aug 18, 2016
2 tasks
@oreoshake
Copy link
Contributor Author

@jacobbednarz are you using this now? I don't have any plans to use this feature at this time and I'm a little uncomfortable deploying a feature I'm not using. I'd love it if this code could bake in a real world scenario for some time.

@jacobbednarz
Copy link
Contributor

I'm happy to get our app running this branch however we are currently only using a report only policy but there are plans in the near future to look at an enforced policy for some of our directives so if you don't mind holding this pull request open for a bit, I'd be happy to get this running in our production environment.

@oreoshake
Copy link
Contributor Author

🆒 I'm in no rush at this time. I'm not using it 😄.

If anyone else has a chance to test drive it, that'd be ✨

@jacobbednarz
Copy link
Contributor

This has been working great in production without any regressions identified using a report-only policy. We are still a couple of weeks off an enforced policy but I will keep you posted.

@oreoshake
Copy link
Contributor Author

I'm going to merge this branch and do a pre-release

@oreoshake oreoshake merged commit 9ee9693 into master Sep 9, 2016
@oreoshake
Copy link
Contributor Author

Released in 3.5.0.pre

@oreoshake oreoshake deleted the dynamic-csp-config-abstraction branch September 9, 2016 00:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants