secure_headers 5.0.4 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +8 -4
  3. data/CHANGELOG.md +8 -0
  4. data/Gemfile +1 -0
  5. data/README.md +2 -2
  6. data/docs/upgrading-to-6-0.md +50 -0
  7. data/lib/secure_headers/configuration.rb +114 -160
  8. data/lib/secure_headers/headers/clear_site_data.rb +2 -4
  9. data/lib/secure_headers/headers/content_security_policy.rb +6 -66
  10. data/lib/secure_headers/headers/content_security_policy_config.rb +3 -13
  11. data/lib/secure_headers/headers/expect_certificate_transparency.rb +2 -3
  12. data/lib/secure_headers/headers/policy_management.rb +14 -65
  13. data/lib/secure_headers/headers/public_key_pins.rb +2 -3
  14. data/lib/secure_headers/headers/referrer_policy.rb +2 -2
  15. data/lib/secure_headers/headers/strict_transport_security.rb +2 -2
  16. data/lib/secure_headers/headers/x_content_type_options.rb +2 -2
  17. data/lib/secure_headers/headers/x_download_options.rb +2 -2
  18. data/lib/secure_headers/headers/x_frame_options.rb +1 -2
  19. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +2 -2
  20. data/lib/secure_headers/headers/x_xss_protection.rb +3 -3
  21. data/lib/secure_headers/view_helper.rb +9 -8
  22. data/lib/secure_headers.rb +14 -78
  23. data/secure_headers.gemspec +1 -11
  24. data/spec/lib/secure_headers/configuration_spec.rb +15 -65
  25. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +2 -65
  26. data/spec/lib/secure_headers/headers/policy_management_spec.rb +35 -9
  27. data/spec/lib/secure_headers/middleware_spec.rb +8 -2
  28. data/spec/lib/secure_headers/view_helpers_spec.rb +30 -1
  29. data/spec/lib/secure_headers_spec.rb +38 -76
  30. data/spec/spec_helper.rb +7 -3
  31. metadata +4 -22
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d30a295775010a197058029cc447bdf51ad1f93e
4
- data.tar.gz: 79a4928de6fe2508ebdfed92cfe36ba1d70cdce8
3
+ metadata.gz: 6d7392744d486b32bda2b91432d39ddfeee87dfd
4
+ data.tar.gz: 38328982bf71376b9412ed8c7d0652163741bf16
5
5
  SHA512:
6
- metadata.gz: 206573552433a49370adf984f538091ac3bd4c2e6ee0ba521f949762e145259725a3938ab2ec5124b5556fdd1502388ac8dd7e8d5fcc947c9f96e3ed78947cfa
7
- data.tar.gz: c53aab77376ad2d8fac21c2d684cc02dcf14322cd51591d7048f0865315a60b6266ce7f066651040b8d626adf311101e31b2b1690ce1e3b18190ea42f90f24ac
6
+ metadata.gz: e08d9df89db6908d0c8419d0086bf8fb6c7e374c53e0bf774bd74d534094c66ef3cdcbac23e97c03b05d4045ba6878c3f6d8a17877146bdb6f2f0c15b57d1784
7
+ data.tar.gz: 304824374c236d11edc52049732a2ea133bbbdf611f35b2c5309a950125e879fb4016f0f15d7ad6c09310ab4b6080e966fac912147f43ed91989aa15898595a7
data/.travis.yml CHANGED
@@ -2,9 +2,10 @@ language: ruby
2
2
 
3
3
  rvm:
4
4
  - ruby-head
5
- - 2.4.2
6
- - 2.3.5
7
- - 2.2.8
5
+ - 2.5.0
6
+ - 2.4.3
7
+ - 2.3.6
8
+ - 2.2.9
8
9
  - jruby-head
9
10
 
10
11
  env:
@@ -18,7 +19,10 @@ matrix:
18
19
  - rvm: jruby-head
19
20
  - rvm: ruby-head
20
21
 
21
- before_install: gem update bundler
22
+ before_install:
23
+ - gem update --system
24
+ - gem --version
25
+ - gem update bundler
22
26
  bundler_args: --without guard -j 3
23
27
 
24
28
  sudo: false
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 6.0
2
+
3
+ - See the [upgrading to 6.0](docs/upgrading-to-6-0.md) guide for the breaking changes.
4
+
5
+ ## 5.0.5
6
+
7
+ - A release to deprecate `SecureHeaders::Configuration#get` in prep for 6.x
8
+
1
9
  ## 5.0.4
2
10
 
3
11
  - Adds support for `nonced_stylesheet_pack_tag` #373 (@paulfri)
data/Gemfile CHANGED
@@ -19,4 +19,5 @@ group :guard do
19
19
  gem "growl"
20
20
  gem "guard-rspec", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22, :ruby_23, :ruby_24]
21
21
  gem "rb-fsevent"
22
+ gem "terminal-notifier-guard"
22
23
  end
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Secure Headers [![Build Status](https://travis-ci.org/twitter/secureheaders.svg?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/twitter/secureheaders)
2
2
 
3
- **master represents 5.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md) and [upgrading to 5.x doc](docs/upgrading-to-5-0.md) for instructions on how to upgrade. Bug fixes should go in the 3.x branch for now.
3
+ **master represents 6.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md), [upgrading to 5.x doc](docs/upgrading-to-5-0.md), or [upgrading to 6.x doc](docs/upgrading-to-6-0.md) for instructions on how to upgrade. Bug fixes should go in the 5.x branch for now.
4
4
 
5
5
  **The [3.x](https://github.com/twitter/secureheaders/tree/2.x) branch is moving into maintenance mode**. See the [upgrading to 3.x doc](docs/upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch.
6
6
 
@@ -77,7 +77,7 @@ SecureHeaders::Configuration.default do |config|
77
77
  preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
78
78
 
79
79
  # directive values: these values will directly translate into source directives
80
- default_src: %w(https: 'self'),
80
+ default_src: %w('none'),
81
81
  base_uri: %w('self'),
82
82
  block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/
83
83
  child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set.
@@ -0,0 +1,50 @@
1
+ ## Named overrides are now dynamically applied
2
+
3
+ The original implementation of name overrides worked by making a copy of the default policy, applying the overrides, and storing the result for later use. But, this lead to unexpected results if named overrides were combined with a dynamic policy change. If a change was made to the default configuration during a request, followed by a named override, the dynamic changes would be lost. To keep things consistent named overrides have been rewritten to work the same as named appends in that they always operate on the configuration for the current request. As an example:
4
+
5
+ ```ruby
6
+ class ApplicationController < ActionController::Base
7
+ Configuration.default do |config|
8
+ config.x_frame_options = OPT_OUT
9
+ end
10
+
11
+ SecureHeaders::Configuration.override(:dynamic_override) do |config|
12
+ config.x_content_type_options = "nosniff"
13
+ end
14
+ end
15
+
16
+ class FooController < ApplicationController
17
+ def bar
18
+ # Dynamically update the default config for this request
19
+ override_x_frame_options("DENY")
20
+ append_content_security_policy_directives(frame_src: "3rdpartyprovider.com")
21
+
22
+ # Override everything, discard modifications above
23
+ use_secure_headers_override(:dynamic_override)
24
+ end
25
+ end
26
+ ```
27
+
28
+ Prior to 6.0.0, the response would NOT include a `X-Frame-Options` header since the named override would be a copy of the default configuration, but with `X-Content-Type-Options` set to `nosniff`. As of 6.0.0, the above code results in both `X-Frame-Options` set to `DENY` AND `X-Content-Type-Options` set to `nosniff`.
29
+
30
+ ## `ContentSecurityPolicyConfig#merge` and `ContentSecurityPolicyReportOnlyConfig#merge` work more like `Hash#merge`
31
+
32
+ These classes are typically not directly instantiated by users of SecureHeaders. But, if you access `config.csp` you end up accessing one of these objects. Prior to 6.0.0, `#merge` worked more like `#append` in that it would combine policies (i.e. if both policies contained the same key the values would be combined rather than overwritten). This was not consistent with `#merge!`, which worked more like ruby's `Hash#merge!` (overwriting duplicate keys). As of 6.0.0, `#merge` works the same as `#merge!`, but returns a new object instead of mutating `self`.
33
+
34
+ ## `Configuration#get` has been removed
35
+
36
+ This method is not typically directly called by users of SecureHeaders. Given that named overrides are no longer statically stored, fetching them no longer makes sense.
37
+
38
+ ## Configuration headers are no longer cached
39
+
40
+ Prior to 6.0.0 SecureHeaders pre-built and cached the headers that corresponded to the default configuration. The same was also done for named overrides. However, now that named overrides are applied dynamically, those can no longer be cached. As a result, caching has been removed in the name of simplicity. Some micro-benchmarks indicate this shouldn't be a performance problem and will help to eliminate a class of bugs entirely.
41
+
42
+ ## Configuration the default configuration more than once will result in an Exception
43
+
44
+ Prior to 6.0.0 you could conceivably, though unlikely, have `Configure#default` called more than once. Because configurations are dynamic, configuring more than once could result in unexpected behavior. So, as of 6.0.0 we raise `AlreadyConfiguredError` if the default configuration is setup more than once.
45
+
46
+ ## All user agent sniffing has been removed
47
+
48
+ The policy configured is the policy that is delivered in terms of which directives are sent. We still dedup, strip schemes, and look for other optimizations but we will not e.g. conditionally send `frame-src` / `child-src` or apply `nonce`s / `unsafe-inline`.
49
+
50
+ The primary reason for these per-browser customization was to reduce console warnings. This has lead to many bugs and results inc confusing behavior. Also, console logs are incredibly noisy today and increasingly warn you about perfectly valid things (like sending `X-Frame-Options` and `frame-ancestors` together).
@@ -4,7 +4,8 @@ require "yaml"
4
4
  module SecureHeaders
5
5
  class Configuration
6
6
  DEFAULT_CONFIG = :default
7
- NOOP_CONFIGURATION = "secure_headers_noop_config"
7
+ NOOP_OVERRIDE = "secure_headers_noop_override"
8
+ class AlreadyConfiguredError < StandardError; end
8
9
  class NotYetConfiguredError < StandardError; end
9
10
  class IllegalPolicyModificationError < StandardError; end
10
11
  class << self
@@ -14,9 +15,21 @@ module SecureHeaders
14
15
  #
15
16
  # Returns the newly created config.
16
17
  def default(&block)
17
- config = new(&block)
18
- add_noop_configuration
19
- add_configuration(DEFAULT_CONFIG, config)
18
+ if defined?(@default_config)
19
+ raise AlreadyConfiguredError, "Policy already configured"
20
+ end
21
+
22
+ # Define a built-in override that clears all configuration options and
23
+ # results in no security headers being set.
24
+ override(NOOP_OVERRIDE) do |config|
25
+ CONFIG_ATTRIBUTES.each do |attr|
26
+ config.instance_variable_set("@#{attr}", OPT_OUT)
27
+ end
28
+ end
29
+
30
+ new_config = new(&block).freeze
31
+ new_config.validate_config!
32
+ @default_config = new_config
20
33
  end
21
34
  alias_method :configure, :default
22
35
 
@@ -27,24 +40,15 @@ module SecureHeaders
27
40
  # if no value is supplied.
28
41
  #
29
42
  # Returns: the newly created config
30
- def override(name, base = DEFAULT_CONFIG, &block)
31
- unless get(base)
32
- raise NotYetConfiguredError, "#{base} policy not yet supplied"
33
- end
34
- override = @configurations[base].dup
35
- override.instance_eval(&block) if block_given?
36
- add_configuration(name, override)
43
+ def override(name, &block)
44
+ @overrides ||= {}
45
+ raise "Provide a configuration block" unless block_given?
46
+ @overrides[name] = block
37
47
  end
38
48
 
39
- # Public: retrieve a global configuration object
40
- #
41
- # Returns the configuration with a given name or raises a
42
- # NotYetConfiguredError if `default` has not been called.
43
- def get(name = DEFAULT_CONFIG)
44
- if @configurations.nil?
45
- raise NotYetConfiguredError, "Default policy not yet supplied"
46
- end
47
- @configurations[name]
49
+ def overrides(name)
50
+ @overrides ||= {}
51
+ @overrides[name]
48
52
  end
49
53
 
50
54
  def named_appends(name)
@@ -52,44 +56,17 @@ module SecureHeaders
52
56
  @appends[name]
53
57
  end
54
58
 
55
- def named_append(name, target = nil, &block)
59
+ def named_append(name, &block)
56
60
  @appends ||= {}
57
61
  raise "Provide a configuration block" unless block_given?
58
62
  @appends[name] = block
59
63
  end
60
64
 
61
- private
62
-
63
- # Private: add a valid configuration to the global set of named configs.
64
- #
65
- # config - the config to store
66
- # name - the lookup value for this config
67
- #
68
- # Raises errors if the config is invalid or if a config named `name`
69
- # already exists.
70
- #
71
- # Returns the config, if valid
72
- def add_configuration(name, config)
73
- config.validate_config!
74
- @configurations ||= {}
75
- config.send(:cache_headers!)
76
- config.send(:cache_hpkp_report_host)
77
- config.freeze
78
- @configurations[name] = config
65
+ def dup
66
+ default_config.dup
79
67
  end
80
68
 
81
- # Private: Automatically add an "opt-out of everything" override.
82
- #
83
- # Returns the noop config
84
- def add_noop_configuration
85
- noop_config = new do |config|
86
- ALL_HEADER_CLASSES.each do |klass|
87
- config.send("#{klass::CONFIG_KEY}=", OPT_OUT)
88
- end
89
- end
90
-
91
- add_configuration(NOOP_CONFIGURATION, noop_config)
92
- end
69
+ private
93
70
 
94
71
  # Public: perform a basic deep dup. The shallow copy provided by dup/clone
95
72
  # can lead to modifying parent objects.
@@ -104,6 +81,17 @@ module SecureHeaders
104
81
  end
105
82
  end
106
83
 
84
+ # Private: Returns the internal default configuration. This should only
85
+ # ever be called by internal callers (or tests) that know the semantics
86
+ # of ensuring that the default config is never mutated and is dup(ed)
87
+ # before it is used in a request.
88
+ def default_config
89
+ unless defined?(@default_config)
90
+ raise NotYetConfiguredError, "Default policy not yet configured"
91
+ end
92
+ @default_config
93
+ end
94
+
107
95
  # Private: convenience method purely DRY things up. The value may not be a
108
96
  # hash (e.g. OPT_OUT, nil)
109
97
  def deep_copy_if_hash(value)
@@ -115,11 +103,31 @@ module SecureHeaders
115
103
  end
116
104
  end
117
105
 
118
- attr_writer :hsts, :x_frame_options, :x_content_type_options,
119
- :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
120
- :referrer_policy, :clear_site_data, :expect_certificate_transparency
106
+ CONFIG_ATTRIBUTES_TO_HEADER_CLASSES = {
107
+ hsts: StrictTransportSecurity,
108
+ x_frame_options: XFrameOptions,
109
+ x_content_type_options: XContentTypeOptions,
110
+ x_xss_protection: XXssProtection,
111
+ x_download_options: XDownloadOptions,
112
+ x_permitted_cross_domain_policies: XPermittedCrossDomainPolicies,
113
+ referrer_policy: ReferrerPolicy,
114
+ clear_site_data: ClearSiteData,
115
+ expect_certificate_transparency: ExpectCertificateTransparency,
116
+ csp: ContentSecurityPolicy,
117
+ csp_report_only: ContentSecurityPolicy,
118
+ hpkp: PublicKeyPins,
119
+ cookies: Cookie,
120
+ }.freeze
121
121
 
122
- attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host
122
+ CONFIG_ATTRIBUTES = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.keys.freeze
123
+
124
+ # The list of attributes that must respond to a `validate_config!` method
125
+ VALIDATABLE_ATTRIBUTES = CONFIG_ATTRIBUTES
126
+
127
+ # The list of attributes that must respond to a `make_header` method
128
+ HEADERABLE_ATTRIBUTES = (CONFIG_ATTRIBUTES - [:cookies]).freeze
129
+
130
+ attr_accessor(*CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.keys)
123
131
 
124
132
  @script_hashes = nil
125
133
  @style_hashes = nil
@@ -136,7 +144,6 @@ module SecureHeaders
136
144
  @clear_site_data = nil
137
145
  @csp = nil
138
146
  @csp_report_only = nil
139
- @hpkp_report_host = nil
140
147
  @hpkp = nil
141
148
  @hsts = nil
142
149
  @x_content_type_options = nil
@@ -154,7 +161,7 @@ module SecureHeaders
154
161
  instance_eval(&block) if block_given?
155
162
  end
156
163
 
157
- # Public: copy everything but the cached headers
164
+ # Public: copy everything
158
165
  #
159
166
  # Returns a deep-dup'd copy of this configuration.
160
167
  def dup
@@ -162,7 +169,6 @@ module SecureHeaders
162
169
  copy.cookies = self.class.send(:deep_copy_if_hash, @cookies)
163
170
  copy.csp = @csp.dup if @csp
164
171
  copy.csp_report_only = @csp_report_only.dup if @csp_report_only
165
- copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
166
172
  copy.x_content_type_options = @x_content_type_options
167
173
  copy.hsts = @hsts
168
174
  copy.x_frame_options = @x_frame_options
@@ -173,18 +179,39 @@ module SecureHeaders
173
179
  copy.expect_certificate_transparency = @expect_certificate_transparency
174
180
  copy.referrer_policy = @referrer_policy
175
181
  copy.hpkp = @hpkp
176
- copy.hpkp_report_host = @hpkp_report_host
177
182
  copy
178
183
  end
179
184
 
185
+ # Public: Apply a named override to the current config
186
+ #
187
+ # Returns self
188
+ def override(name = nil, &block)
189
+ if override = self.class.overrides(name)
190
+ instance_eval(&override)
191
+ else
192
+ raise ArgumentError.new("no override by the name of #{name} has been configured")
193
+ end
194
+ self
195
+ end
196
+
197
+ def generate_headers
198
+ headers = {}
199
+ HEADERABLE_ATTRIBUTES.each do |attr|
200
+ klass = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES[attr]
201
+ header_name, value = klass.make_header(instance_variable_get("@#{attr}"))
202
+ if header_name && value
203
+ headers[header_name] = value
204
+ end
205
+ end
206
+ headers
207
+ end
208
+
180
209
  def opt_out(header)
181
210
  send("#{header}=", OPT_OUT)
182
- self.cached_headers.delete(header)
183
211
  end
184
212
 
185
213
  def update_x_frame_options(value)
186
214
  @x_frame_options = value
187
- self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value)
188
215
  end
189
216
 
190
217
  # Public: validates all configurations values.
@@ -193,19 +220,10 @@ module SecureHeaders
193
220
  #
194
221
  # Returns nothing
195
222
  def validate_config!
196
- StrictTransportSecurity.validate_config!(@hsts)
197
- ContentSecurityPolicy.validate_config!(@csp)
198
- ContentSecurityPolicy.validate_config!(@csp_report_only)
199
- ReferrerPolicy.validate_config!(@referrer_policy)
200
- XFrameOptions.validate_config!(@x_frame_options)
201
- XContentTypeOptions.validate_config!(@x_content_type_options)
202
- XXssProtection.validate_config!(@x_xss_protection)
203
- XDownloadOptions.validate_config!(@x_download_options)
204
- XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
205
- ClearSiteData.validate_config!(@clear_site_data)
206
- ExpectCertificateTransparency.validate_config!(@expect_certificate_transparency)
207
- PublicKeyPins.validate_config!(@hpkp)
208
- Cookie.validate_config!(@cookies)
223
+ VALIDATABLE_ATTRIBUTES.each do |attr|
224
+ klass = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES[attr]
225
+ klass.validate_config!(instance_variable_get("@#{attr}"))
226
+ end
209
227
  end
210
228
 
211
229
  def secure_cookies=(secure_cookies)
@@ -213,15 +231,15 @@ module SecureHeaders
213
231
  end
214
232
 
215
233
  def csp=(new_csp)
216
- if new_csp.respond_to?(:opt_out?)
217
- @csp = new_csp.dup
234
+ case new_csp
235
+ when OPT_OUT
236
+ @csp = new_csp
237
+ when ContentSecurityPolicyConfig
238
+ @csp = new_csp
239
+ when Hash
240
+ @csp = ContentSecurityPolicyConfig.new(new_csp)
218
241
  else
219
- if new_csp[:report_only]
220
- # invalid configuration implies that CSPRO should be set, CSP should not - so opt out
221
- raise ArgumentError, "#{Kernel.caller.first}: `#csp=` was supplied a config with report_only: true. Use #csp_report_only="
222
- else
223
- @csp = ContentSecurityPolicyConfig.new(new_csp)
224
- end
242
+ raise ArgumentError, "Must provide either an existing CSP config or a CSP config hash"
225
243
  end
226
244
  end
227
245
 
@@ -232,87 +250,23 @@ module SecureHeaders
232
250
  # configuring csp_report_only, the code will assume you mean to only use
233
251
  # report-only mode and you will be opted-out of enforce mode.
234
252
  def csp_report_only=(new_csp)
235
- @csp_report_only = begin
236
- if new_csp.is_a?(ContentSecurityPolicyConfig)
237
- new_csp.make_report_only
238
- elsif new_csp.respond_to?(:opt_out?)
239
- new_csp.dup
240
- else
241
- if new_csp[:report_only] == false # nil is a valid value on which we do not want to raise
242
- raise ContentSecurityPolicyConfigError, "`#csp_report_only=` was supplied a config with report_only: false. Use #csp="
243
- else
244
- ContentSecurityPolicyReportOnlyConfig.new(new_csp)
245
- end
246
- end
247
- end
248
- end
249
-
250
- protected
251
-
252
- def cookies=(cookies)
253
- @cookies = cookies
254
- end
255
-
256
- def cached_headers=(headers)
257
- @cached_headers = headers
258
- end
259
-
260
- def hpkp=(hpkp)
261
- @hpkp = self.class.send(:deep_copy_if_hash, hpkp)
262
- end
263
-
264
- def hpkp_report_host=(hpkp_report_host)
265
- @hpkp_report_host = hpkp_report_host
266
- end
267
-
268
- private
269
-
270
- def cache_hpkp_report_host
271
- has_report_uri = @hpkp && @hpkp != OPT_OUT && @hpkp[:report_uri]
272
- self.hpkp_report_host = if has_report_uri
273
- parsed_report_uri = URI.parse(@hpkp[:report_uri])
274
- parsed_report_uri.host
253
+ case new_csp
254
+ when OPT_OUT
255
+ @csp_report_only = new_csp
256
+ when ContentSecurityPolicyReportOnlyConfig
257
+ @csp_report_only = new_csp.dup
258
+ when ContentSecurityPolicyConfig
259
+ @csp_report_only = new_csp.make_report_only
260
+ when Hash
261
+ @csp_report_only = ContentSecurityPolicyReportOnlyConfig.new(new_csp)
262
+ else
263
+ raise ArgumentError, "Must provide either an existing CSP config or a CSP config hash"
275
264
  end
276
265
  end
277
266
 
278
- # Public: Precompute the header names and values for this configuration.
279
- # Ensures that headers generated at configure time, not on demand.
280
- #
281
- # Returns the cached headers
282
- def cache_headers!
283
- # generate defaults for the "easy" headers
284
- headers = (ALL_HEADERS_BESIDES_CSP).each_with_object({}) do |klass, hash|
285
- config = instance_variable_get("@#{klass::CONFIG_KEY}")
286
- unless config == OPT_OUT
287
- hash[klass::CONFIG_KEY] = klass.make_header(config).freeze
288
- end
289
- end
290
-
291
- generate_csp_headers(headers)
292
-
293
- headers.freeze
294
- self.cached_headers = headers
295
- end
296
-
297
- # Private: adds CSP headers for each variation of CSP support.
298
- #
299
- # headers - generated headers are added to this hash namespaced by The
300
- # different variations
301
- #
302
- # Returns nothing
303
- def generate_csp_headers(headers)
304
- generate_csp_headers_for_config(headers, ContentSecurityPolicyConfig::CONFIG_KEY, self.csp)
305
- generate_csp_headers_for_config(headers, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY, self.csp_report_only)
306
- end
307
-
308
- def generate_csp_headers_for_config(headers, header_key, csp_config)
309
- unless csp_config.opt_out?
310
- headers[header_key] = {}
311
- ContentSecurityPolicy::VARIATIONS.each_key do |name|
312
- csp = ContentSecurityPolicy.make_header(csp_config, UserAgent.parse(name))
313
- headers[header_key][name] = csp.freeze
314
- end
315
- end
267
+ def hpkp_report_host
268
+ return nil unless @hpkp && hpkp != OPT_OUT && @hpkp[:report_uri]
269
+ URI.parse(@hpkp[:report_uri]).host
316
270
  end
317
271
  end
318
272
  end
@@ -11,13 +11,11 @@ module SecureHeaders
11
11
  EXECUTION_CONTEXTS = "executionContexts".freeze
12
12
  ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECUTION_CONTEXTS]
13
13
 
14
- CONFIG_KEY = :clear_site_data
15
-
16
14
  class << self
17
15
  # Public: make an Clear-Site-Data header name, value pair
18
16
  #
19
17
  # Returns nil if not configured, returns header name and value if configured.
20
- def make_header(config = nil)
18
+ def make_header(config = nil, user_agent = nil)
21
19
  case config
22
20
  when nil, OPT_OUT, []
23
21
  # noop
@@ -48,7 +46,7 @@ module SecureHeaders
48
46
  #
49
47
  # Returns a String of quoted values that are comma separated.
50
48
  def make_header_value(types)
51
- types.map { |t| "\"#{t}\""}.join(", ")
49
+ types.map { |t| %("#{t}") }.join(", ")
52
50
  end
53
51
  end
54
52
  end
@@ -1,18 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
  require_relative "policy_management"
3
3
  require_relative "content_security_policy_config"
4
- require "useragent"
5
4
 
6
5
  module SecureHeaders
7
6
  class ContentSecurityPolicy
8
7
  include PolicyManagement
9
8
 
10
- # constants to be used for version-specific UA sniffing
11
- VERSION_46 = ::UserAgent::Version.new("46")
12
- VERSION_10 = ::UserAgent::Version.new("10")
13
- FALLBACK_VERSION = ::UserAgent::Version.new("0")
14
-
15
- def initialize(config = nil, user_agent = OTHER)
9
+ def initialize(config = nil)
16
10
  @config = if config.is_a?(Hash)
17
11
  if config[:report_only]
18
12
  ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
@@ -25,12 +19,6 @@ module SecureHeaders
25
19
  config
26
20
  end
27
21
 
28
- @parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
29
- user_agent
30
- else
31
- UserAgent.parse(user_agent)
32
- end
33
- @frame_src = normalize_child_frame_src
34
22
  @preserve_schemes = @config.preserve_schemes
35
23
  @script_nonce = @config.script_nonce
36
24
  @style_nonce = @config.style_nonce
@@ -55,20 +43,10 @@ module SecureHeaders
55
43
 
56
44
  private
57
45
 
58
- def normalize_child_frame_src
59
- if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src
60
- raise ArgumentError, "#{Kernel.caller.first}: both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers."
61
- end
62
-
63
- @config.frame_src || @config.child_src
64
- end
65
-
66
46
  # Private: converts the config object into a string representing a policy.
67
47
  # Places default-src at the first directive and report-uri as the last. All
68
48
  # others are presented in alphabetical order.
69
49
  #
70
- # Unsupported directives are filtered based on the user agent.
71
- #
72
50
  # Returns a content security policy header value.
73
51
  def build_value
74
52
  directives.map do |directive_name|
@@ -124,18 +102,7 @@ module SecureHeaders
124
102
  #
125
103
  # Returns a string representing a directive.
126
104
  def build_source_list_directive(directive)
127
- source_list = case directive
128
- when :child_src
129
- if supported_directives.include?(:child_src)
130
- @frame_src
131
- end
132
- when :frame_src
133
- unless supported_directives.include?(:child_src)
134
- @frame_src
135
- end
136
- else
137
- @config.directive_value(directive)
138
- end
105
+ source_list = @config.directive_value(directive)
139
106
 
140
107
  if source_list != OPT_OUT && source_list && source_list.any?
141
108
  normalized_source_list = minify_source_list(directive, source_list)
@@ -212,23 +179,19 @@ module SecureHeaders
212
179
  # unsafe-inline, this is more concise.
213
180
  def append_nonce(source_list, nonce)
214
181
  if nonce
215
- if nonces_supported?
216
- source_list << "'nonce-#{nonce}'"
217
- else
218
- source_list << UNSAFE_INLINE
219
- end
182
+ source_list.push("'nonce-#{nonce}'", UNSAFE_INLINE)
220
183
  end
221
184
 
222
185
  source_list
223
186
  end
224
187
 
225
- # Private: return the list of directives that are supported by the user agent,
188
+ # Private: return the list of directives,
226
189
  # starting with default-src and ending with report-uri.
227
190
  def directives
228
191
  [
229
192
  DEFAULT_SRC,
230
- BODY_DIRECTIVES.select { |key| supported_directives.include?(key) },
231
- REPORT_URI
193
+ BODY_DIRECTIVES,
194
+ REPORT_URI,
232
195
  ].flatten
233
196
  end
234
197
 
@@ -237,29 +200,6 @@ module SecureHeaders
237
200
  source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
238
201
  end
239
202
 
240
- # Private: determine which directives are supported for the given user agent.
241
- #
242
- # Add UA-sniffing special casing here.
243
- #
244
- # Returns an array of symbols representing the directives.
245
- def supported_directives
246
- @supported_directives ||= if VARIATIONS[@parsed_ua.browser]
247
- if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46)
248
- VARIATIONS["FirefoxTransitional"]
249
- elsif @parsed_ua.browser == "Safari" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_10)
250
- VARIATIONS["SafariTransitional"]
251
- else
252
- VARIATIONS[@parsed_ua.browser]
253
- end
254
- else
255
- VARIATIONS[OTHER]
256
- end
257
- end
258
-
259
- def nonces_supported?
260
- @nonces_supported ||= self.class.nonces_supported?(@parsed_ua)
261
- end
262
-
263
203
  def symbol_to_hyphen_case(sym)
264
204
  sym.to_s.tr("_", "-")
265
205
  end