secure_headers 5.1.0 → 6.0.0.alpha01

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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +8 -4
  3. data/CHANGELOG.md +3 -3
  4. data/Gemfile +1 -1
  5. data/docs/upgrading-to-6-0.md +50 -0
  6. data/lib/secure_headers/configuration.rb +114 -164
  7. data/lib/secure_headers/headers/clear_site_data.rb +1 -3
  8. data/lib/secure_headers/headers/content_security_policy.rb +4 -17
  9. data/lib/secure_headers/headers/content_security_policy_config.rb +3 -13
  10. data/lib/secure_headers/headers/expect_certificate_transparency.rb +2 -3
  11. data/lib/secure_headers/headers/policy_management.rb +12 -11
  12. data/lib/secure_headers/headers/public_key_pins.rb +2 -3
  13. data/lib/secure_headers/headers/referrer_policy.rb +2 -2
  14. data/lib/secure_headers/headers/strict_transport_security.rb +2 -2
  15. data/lib/secure_headers/headers/x_content_type_options.rb +2 -2
  16. data/lib/secure_headers/headers/x_download_options.rb +2 -2
  17. data/lib/secure_headers/headers/x_frame_options.rb +1 -2
  18. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +2 -2
  19. data/lib/secure_headers/headers/x_xss_protection.rb +3 -3
  20. data/lib/secure_headers.rb +14 -76
  21. data/secure_headers.gemspec +1 -1
  22. data/spec/lib/secure_headers/configuration_spec.rb +15 -70
  23. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +12 -17
  24. data/spec/lib/secure_headers/headers/policy_management_spec.rb +33 -7
  25. data/spec/lib/secure_headers/middleware_spec.rb +7 -1
  26. data/spec/lib/secure_headers/view_helpers_spec.rb +1 -0
  27. data/spec/lib/secure_headers_spec.rb +39 -40
  28. data/spec/spec_helper.rb +7 -3
  29. metadata +5 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 32ea96aca183a8341bec40b998486beed14d59e8
4
- data.tar.gz: 3081354f4bd486f5ea49d22f680d1ac2d36de4aa
3
+ metadata.gz: cd74fc8e9c9057255c1e6c75bc78554ec6751459
4
+ data.tar.gz: 2f72e782c0800b5bf124d841a0d556d321b1cb5f
5
5
  SHA512:
6
- metadata.gz: cc8c64abdabaab458be788ee09a18984bab17cb8a01ef6eb76c75099cfaa5b8fc5962f5094e39f7067ebece388794e7f53a2e44f2fecdbfe4146a59a981211df
7
- data.tar.gz: f8c369a5a2ac0e7547e231100c2a946a9204684d4db33206cc3baf0dd786b99c5a6d05cdaaaca5fcb439530a6d385d029c265dbc84f9b6ff665df52ad1191537
6
+ metadata.gz: 53ff0d4849c3892fbf4e35270c011d9d6c8da3984958fdc17f9669b32f0d9d39542dc4411323fe93bbf21137f0c8cfe26565a5836548061414153a2abd2dd949
7
+ data.tar.gz: 7472a097ae0926655aace8f5f719a4306920b08e4a1b798a588034d51a1446f51f442deb7c3f298d23481d52f2aa1b210ffb40b629671f449f63b7570a7ec2b0
data/.travis.yml CHANGED
@@ -2,9 +2,10 @@ language: ruby
2
2
 
3
3
  rvm:
4
4
  - ruby-head
5
- - 2.6
6
- - 2.5
7
- - 2.4
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,7 +1,7 @@
1
- ## 5.1.0
2
-
3
- Fixes semicolon injection issue reported by @mvgijssel see https://github.com/twitter/secure_headers/issues/418
1
+ ## 6.0
4
2
 
3
+ - See the [upgrading to 6.0](docs/upgrading-to-6-0.md) guide for the breaking changes.
4
+ =======
5
5
  ## 5.0.5
6
6
 
7
7
  A release to deprecate `SecureHeaders::Configuration#get` in prep for 6.x
data/Gemfile CHANGED
@@ -9,7 +9,7 @@ group :test do
9
9
  gem "pry-nav"
10
10
  gem "rack"
11
11
  gem "rspec"
12
- gem "rubocop", "< 0.68"
12
+ gem "rubocop"
13
13
  gem "rubocop-github"
14
14
  gem "term-ansicolor"
15
15
  gem "tins"
@@ -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
+ ## Nonce behavior and console warnings
47
+
48
+ Since the first commit, reducing browser console messages was a goal. It led to overly complicated and error-prone UA sniffing. Nowadays, consoles warn on completely legitimate use of features meant to be backwards compatible. So the goal is impossible and the impact is negative, so eliminating code using sniffing is a goal.
49
+
50
+ The first example: we will now send `'unsafe-inline'` along with nonce source expressions. This will generate warnings in some consoles but is 100% valid use and was a design goal of CSP in the early days. The concept of versioning CSP lost out and so we're left with backward compatibility as our only option.
@@ -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,28 +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, internal: true)
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, internal: false)
44
- unless internal
45
- Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#get` is deprecated. It will be removed in the next major release. Use SecureHeaders::Configuration.dup to retrieve the default config."
46
- end
47
-
48
- if @configurations.nil?
49
- raise NotYetConfiguredError, "Default policy not yet supplied"
50
- end
51
- @configurations[name]
49
+ def overrides(name)
50
+ @overrides ||= {}
51
+ @overrides[name]
52
52
  end
53
53
 
54
54
  def named_appends(name)
@@ -56,44 +56,17 @@ module SecureHeaders
56
56
  @appends[name]
57
57
  end
58
58
 
59
- def named_append(name, target = nil, &block)
59
+ def named_append(name, &block)
60
60
  @appends ||= {}
61
61
  raise "Provide a configuration block" unless block_given?
62
62
  @appends[name] = block
63
63
  end
64
64
 
65
- private
66
-
67
- # Private: add a valid configuration to the global set of named configs.
68
- #
69
- # config - the config to store
70
- # name - the lookup value for this config
71
- #
72
- # Raises errors if the config is invalid or if a config named `name`
73
- # already exists.
74
- #
75
- # Returns the config, if valid
76
- def add_configuration(name, config)
77
- config.validate_config!
78
- @configurations ||= {}
79
- config.send(:cache_headers!)
80
- config.send(:cache_hpkp_report_host)
81
- config.freeze
82
- @configurations[name] = config
65
+ def dup
66
+ default_config.dup
83
67
  end
84
68
 
85
- # Private: Automatically add an "opt-out of everything" override.
86
- #
87
- # Returns the noop config
88
- def add_noop_configuration
89
- noop_config = new do |config|
90
- ALL_HEADER_CLASSES.each do |klass|
91
- config.send("#{klass::CONFIG_KEY}=", OPT_OUT)
92
- end
93
- end
94
-
95
- add_configuration(NOOP_CONFIGURATION, noop_config)
96
- end
69
+ private
97
70
 
98
71
  # Public: perform a basic deep dup. The shallow copy provided by dup/clone
99
72
  # can lead to modifying parent objects.
@@ -108,6 +81,17 @@ module SecureHeaders
108
81
  end
109
82
  end
110
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
+
111
95
  # Private: convenience method purely DRY things up. The value may not be a
112
96
  # hash (e.g. OPT_OUT, nil)
113
97
  def deep_copy_if_hash(value)
@@ -119,11 +103,31 @@ module SecureHeaders
119
103
  end
120
104
  end
121
105
 
122
- attr_writer :hsts, :x_frame_options, :x_content_type_options,
123
- :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
124
- :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
+
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
125
126
 
126
- attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host
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)
127
131
 
128
132
  @script_hashes = nil
129
133
  @style_hashes = nil
@@ -140,7 +144,6 @@ module SecureHeaders
140
144
  @clear_site_data = nil
141
145
  @csp = nil
142
146
  @csp_report_only = nil
143
- @hpkp_report_host = nil
144
147
  @hpkp = nil
145
148
  @hsts = nil
146
149
  @x_content_type_options = nil
@@ -158,7 +161,7 @@ module SecureHeaders
158
161
  instance_eval(&block) if block_given?
159
162
  end
160
163
 
161
- # Public: copy everything but the cached headers
164
+ # Public: copy everything
162
165
  #
163
166
  # Returns a deep-dup'd copy of this configuration.
164
167
  def dup
@@ -166,7 +169,6 @@ module SecureHeaders
166
169
  copy.cookies = self.class.send(:deep_copy_if_hash, @cookies)
167
170
  copy.csp = @csp.dup if @csp
168
171
  copy.csp_report_only = @csp_report_only.dup if @csp_report_only
169
- copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
170
172
  copy.x_content_type_options = @x_content_type_options
171
173
  copy.hsts = @hsts
172
174
  copy.x_frame_options = @x_frame_options
@@ -177,18 +179,39 @@ module SecureHeaders
177
179
  copy.expect_certificate_transparency = @expect_certificate_transparency
178
180
  copy.referrer_policy = @referrer_policy
179
181
  copy.hpkp = @hpkp
180
- copy.hpkp_report_host = @hpkp_report_host
181
182
  copy
182
183
  end
183
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(user_agent)
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}"), user_agent)
202
+ if header_name && value
203
+ headers[header_name] = value
204
+ end
205
+ end
206
+ headers
207
+ end
208
+
184
209
  def opt_out(header)
185
210
  send("#{header}=", OPT_OUT)
186
- self.cached_headers.delete(header)
187
211
  end
188
212
 
189
213
  def update_x_frame_options(value)
190
214
  @x_frame_options = value
191
- self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value)
192
215
  end
193
216
 
194
217
  # Public: validates all configurations values.
@@ -197,19 +220,10 @@ module SecureHeaders
197
220
  #
198
221
  # Returns nothing
199
222
  def validate_config!
200
- StrictTransportSecurity.validate_config!(@hsts)
201
- ContentSecurityPolicy.validate_config!(@csp)
202
- ContentSecurityPolicy.validate_config!(@csp_report_only)
203
- ReferrerPolicy.validate_config!(@referrer_policy)
204
- XFrameOptions.validate_config!(@x_frame_options)
205
- XContentTypeOptions.validate_config!(@x_content_type_options)
206
- XXssProtection.validate_config!(@x_xss_protection)
207
- XDownloadOptions.validate_config!(@x_download_options)
208
- XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
209
- ClearSiteData.validate_config!(@clear_site_data)
210
- ExpectCertificateTransparency.validate_config!(@expect_certificate_transparency)
211
- PublicKeyPins.validate_config!(@hpkp)
212
- 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
213
227
  end
214
228
 
215
229
  def secure_cookies=(secure_cookies)
@@ -217,15 +231,15 @@ module SecureHeaders
217
231
  end
218
232
 
219
233
  def csp=(new_csp)
220
- if new_csp.respond_to?(:opt_out?)
221
- @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)
222
241
  else
223
- if new_csp[:report_only]
224
- # invalid configuration implies that CSPRO should be set, CSP should not - so opt out
225
- raise ArgumentError, "#{Kernel.caller.first}: `#csp=` was supplied a config with report_only: true. Use #csp_report_only="
226
- else
227
- @csp = ContentSecurityPolicyConfig.new(new_csp)
228
- end
242
+ raise ArgumentError, "Must provide either an existing CSP config or a CSP config hash"
229
243
  end
230
244
  end
231
245
 
@@ -236,87 +250,23 @@ module SecureHeaders
236
250
  # configuring csp_report_only, the code will assume you mean to only use
237
251
  # report-only mode and you will be opted-out of enforce mode.
238
252
  def csp_report_only=(new_csp)
239
- @csp_report_only = begin
240
- if new_csp.is_a?(ContentSecurityPolicyConfig)
241
- new_csp.make_report_only
242
- elsif new_csp.respond_to?(:opt_out?)
243
- new_csp.dup
244
- else
245
- if new_csp[:report_only] == false # nil is a valid value on which we do not want to raise
246
- raise ContentSecurityPolicyConfigError, "`#csp_report_only=` was supplied a config with report_only: false. Use #csp="
247
- else
248
- ContentSecurityPolicyReportOnlyConfig.new(new_csp)
249
- end
250
- end
251
- end
252
- end
253
-
254
- protected
255
-
256
- def cookies=(cookies)
257
- @cookies = cookies
258
- end
259
-
260
- def cached_headers=(headers)
261
- @cached_headers = headers
262
- end
263
-
264
- def hpkp=(hpkp)
265
- @hpkp = self.class.send(:deep_copy_if_hash, hpkp)
266
- end
267
-
268
- def hpkp_report_host=(hpkp_report_host)
269
- @hpkp_report_host = hpkp_report_host
270
- end
271
-
272
- private
273
-
274
- def cache_hpkp_report_host
275
- has_report_uri = @hpkp && @hpkp != OPT_OUT && @hpkp[:report_uri]
276
- self.hpkp_report_host = if has_report_uri
277
- parsed_report_uri = URI.parse(@hpkp[:report_uri])
278
- parsed_report_uri.host
279
- end
280
- end
281
-
282
- # Public: Precompute the header names and values for this configuration.
283
- # Ensures that headers generated at configure time, not on demand.
284
- #
285
- # Returns the cached headers
286
- def cache_headers!
287
- # generate defaults for the "easy" headers
288
- headers = (ALL_HEADERS_BESIDES_CSP).each_with_object({}) do |klass, hash|
289
- config = instance_variable_get("@#{klass::CONFIG_KEY}")
290
- unless config == OPT_OUT
291
- hash[klass::CONFIG_KEY] = klass.make_header(config).freeze
292
- end
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"
293
264
  end
294
-
295
- generate_csp_headers(headers)
296
-
297
- headers.freeze
298
- self.cached_headers = headers
299
265
  end
300
266
 
301
- # Private: adds CSP headers for each variation of CSP support.
302
- #
303
- # headers - generated headers are added to this hash namespaced by The
304
- # different variations
305
- #
306
- # Returns nothing
307
- def generate_csp_headers(headers)
308
- generate_csp_headers_for_config(headers, ContentSecurityPolicyConfig::CONFIG_KEY, self.csp)
309
- generate_csp_headers_for_config(headers, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY, self.csp_report_only)
310
- end
311
-
312
- def generate_csp_headers_for_config(headers, header_key, csp_config)
313
- unless csp_config.opt_out?
314
- headers[header_key] = {}
315
- ContentSecurityPolicy::VARIATIONS.each_key do |name|
316
- csp = ContentSecurityPolicy.make_header(csp_config, UserAgent.parse(name))
317
- headers[header_key][name] = csp.freeze
318
- end
319
- end
267
+ def hpkp_report_host
268
+ return nil unless @hpkp && hpkp != OPT_OUT && @hpkp[:report_uri]
269
+ URI.parse(@hpkp[:report_uri]).host
320
270
  end
321
271
  end
322
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
@@ -13,6 +13,7 @@ module SecureHeaders
13
13
  FALLBACK_VERSION = ::UserAgent::Version.new("0")
14
14
 
15
15
  def initialize(config = nil, user_agent = OTHER)
16
+ user_agent ||= OTHER
16
17
  @config = if config.is_a?(Hash)
17
18
  if config[:report_only]
18
19
  ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
@@ -138,14 +139,8 @@ module SecureHeaders
138
139
  end
139
140
 
140
141
  if source_list != OPT_OUT && source_list && source_list.any?
141
- minified_source_list = minify_source_list(directive, source_list).join(" ")
142
-
143
- if minified_source_list.include?(";")
144
- Kernel.warn("#{directive} contains a ; in '#{minified_source_list}' which will raise an error in future versions. It has been replaced with a blank space.")
145
- end
146
-
147
- escaped_source_list = minified_source_list.gsub(";", " ")
148
- [symbol_to_hyphen_case(directive), escaped_source_list].join(" ").strip
142
+ normalized_source_list = minify_source_list(directive, source_list)
143
+ [symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
149
144
  end
150
145
  end
151
146
 
@@ -218,11 +213,7 @@ module SecureHeaders
218
213
  # unsafe-inline, this is more concise.
219
214
  def append_nonce(source_list, nonce)
220
215
  if nonce
221
- if nonces_supported?
222
- source_list << "'nonce-#{nonce}'"
223
- else
224
- source_list << UNSAFE_INLINE
225
- end
216
+ source_list.push("'nonce-#{nonce}'", UNSAFE_INLINE)
226
217
  end
227
218
 
228
219
  source_list
@@ -262,10 +253,6 @@ module SecureHeaders
262
253
  end
263
254
  end
264
255
 
265
- def nonces_supported?
266
- @nonces_supported ||= self.class.nonces_supported?(@parsed_ua)
267
- end
268
-
269
256
  def symbol_to_hyphen_case(sym)
270
257
  sym.to_s.tr("_", "-")
271
258
  end
@@ -2,7 +2,6 @@
2
2
  module SecureHeaders
3
3
  module DynamicConfig
4
4
  def self.included(base)
5
- base.send(:attr_writer, :modified)
6
5
  base.send(:attr_reader, *base.attrs)
7
6
  base.attrs.each do |attr|
8
7
  base.send(:define_method, "#{attr}=") do |value|
@@ -42,7 +41,6 @@ module SecureHeaders
42
41
  @upgrade_insecure_requests = nil
43
42
 
44
43
  from_hash(hash)
45
- @modified = false
46
44
  end
47
45
 
48
46
  def update_directive(directive, value)
@@ -55,12 +53,10 @@ module SecureHeaders
55
53
  end
56
54
  end
57
55
 
58
- def modified?
59
- @modified
60
- end
61
-
62
56
  def merge(new_hash)
63
- ContentSecurityPolicy.combine_policies(self.to_h, new_hash)
57
+ new_config = self.dup
58
+ new_config.send(:from_hash, new_hash)
59
+ new_config
64
60
  end
65
61
 
66
62
  def merge!(new_hash)
@@ -109,17 +105,12 @@ module SecureHeaders
109
105
  def write_attribute(attr, value)
110
106
  value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
111
107
  attr_variable = "@#{attr}"
112
- prev_value = self.instance_variable_get(attr_variable)
113
108
  self.instance_variable_set(attr_variable, value)
114
- if prev_value != value
115
- @modified = true
116
- end
117
109
  end
118
110
  end
119
111
 
120
112
  class ContentSecurityPolicyConfigError < StandardError; end
121
113
  class ContentSecurityPolicyConfig
122
- CONFIG_KEY = :csp
123
114
  HEADER_NAME = "Content-Security-Policy".freeze
124
115
 
125
116
  ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
@@ -149,7 +140,6 @@ module SecureHeaders
149
140
  end
150
141
 
151
142
  class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig
152
- CONFIG_KEY = :csp_report_only
153
143
  HEADER_NAME = "Content-Security-Policy-Report-Only".freeze
154
144
 
155
145
  def report_only?
@@ -4,7 +4,6 @@ module SecureHeaders
4
4
 
5
5
  class ExpectCertificateTransparency
6
6
  HEADER_NAME = "Expect-CT".freeze
7
- CONFIG_KEY = :expect_certificate_transparency
8
7
  INVALID_CONFIGURATION_ERROR = "config must be a hash.".freeze
9
8
  INVALID_ENFORCE_VALUE_ERROR = "enforce must be a boolean".freeze
10
9
  REQUIRED_MAX_AGE_ERROR = "max-age is a required directive.".freeze
@@ -15,8 +14,8 @@ module SecureHeaders
15
14
  #
16
15
  # Returns nil if not configured, returns header name and value if
17
16
  # configured.
18
- def make_header(config)
19
- return if config.nil?
17
+ def make_header(config, use_agent = nil)
18
+ return if config.nil? || config == OPT_OUT
20
19
 
21
20
  header = new(config)
22
21
  [HEADER_NAME, header.value]