secure_headers 5.1.0 → 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 +3 -3
  4. data/Gemfile +1 -1
  5. data/README.md +2 -2
  6. data/docs/upgrading-to-6-0.md +50 -0
  7. data/lib/secure_headers/configuration.rb +114 -164
  8. data/lib/secure_headers/headers/clear_site_data.rb +1 -3
  9. data/lib/secure_headers/headers/content_security_policy.rb +8 -74
  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 -2
  24. data/spec/lib/secure_headers/configuration_spec.rb +15 -70
  25. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +2 -70
  26. data/spec/lib/secure_headers/headers/policy_management_spec.rb +35 -9
  27. data/spec/lib/secure_headers/middleware_spec.rb +7 -1
  28. data/spec/lib/secure_headers/view_helpers_spec.rb +29 -0
  29. data/spec/lib/secure_headers_spec.rb +38 -76
  30. data/spec/spec_helper.rb +7 -3
  31. metadata +3 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 32ea96aca183a8341bec40b998486beed14d59e8
4
- data.tar.gz: 3081354f4bd486f5ea49d22f680d1ac2d36de4aa
3
+ metadata.gz: 6d7392744d486b32bda2b91432d39ddfeee87dfd
4
+ data.tar.gz: 38328982bf71376b9412ed8c7d0652163741bf16
5
5
  SHA512:
6
- metadata.gz: cc8c64abdabaab458be788ee09a18984bab17cb8a01ef6eb76c75099cfaa5b8fc5962f5094e39f7067ebece388794e7f53a2e44f2fecdbfe4146a59a981211df
7
- data.tar.gz: f8c369a5a2ac0e7547e231100c2a946a9204684d4db33206cc3baf0dd786b99c5a6d05cdaaaca5fcb439530a6d385d029c265dbc84f9b6ff665df52ad1191537
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.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,10 +1,10 @@
1
- ## 5.1.0
1
+ ## 6.0
2
2
 
3
- Fixes semicolon injection issue reported by @mvgijssel see https://github.com/twitter/secure_headers/issues/418
3
+ - See the [upgrading to 6.0](docs/upgrading-to-6-0.md) guide for the breaking changes.
4
4
 
5
5
  ## 5.0.5
6
6
 
7
- A release to deprecate `SecureHeaders::Configuration#get` in prep for 6.x
7
+ - A release to deprecate `SecureHeaders::Configuration#get` in prep for 6.x
8
8
 
9
9
  ## 5.0.4
10
10
 
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"
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,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
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
+
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
@@ -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,28 +102,11 @@ 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
- 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
108
+ normalized_source_list = minify_source_list(directive, source_list)
109
+ [symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
149
110
  end
150
111
  end
151
112
 
@@ -218,23 +179,19 @@ module SecureHeaders
218
179
  # unsafe-inline, this is more concise.
219
180
  def append_nonce(source_list, nonce)
220
181
  if nonce
221
- if nonces_supported?
222
- source_list << "'nonce-#{nonce}'"
223
- else
224
- source_list << UNSAFE_INLINE
225
- end
182
+ source_list.push("'nonce-#{nonce}'", UNSAFE_INLINE)
226
183
  end
227
184
 
228
185
  source_list
229
186
  end
230
187
 
231
- # Private: return the list of directives that are supported by the user agent,
188
+ # Private: return the list of directives,
232
189
  # starting with default-src and ending with report-uri.
233
190
  def directives
234
191
  [
235
192
  DEFAULT_SRC,
236
- BODY_DIRECTIVES.select { |key| supported_directives.include?(key) },
237
- REPORT_URI
193
+ BODY_DIRECTIVES,
194
+ REPORT_URI,
238
195
  ].flatten
239
196
  end
240
197
 
@@ -243,29 +200,6 @@ module SecureHeaders
243
200
  source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
244
201
  end
245
202
 
246
- # Private: determine which directives are supported for the given user agent.
247
- #
248
- # Add UA-sniffing special casing here.
249
- #
250
- # Returns an array of symbols representing the directives.
251
- def supported_directives
252
- @supported_directives ||= if VARIATIONS[@parsed_ua.browser]
253
- if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46)
254
- VARIATIONS["FirefoxTransitional"]
255
- elsif @parsed_ua.browser == "Safari" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_10)
256
- VARIATIONS["SafariTransitional"]
257
- else
258
- VARIATIONS[@parsed_ua.browser]
259
- end
260
- else
261
- VARIATIONS[OTHER]
262
- end
263
- end
264
-
265
- def nonces_supported?
266
- @nonces_supported ||= self.class.nonces_supported?(@parsed_ua)
267
- end
268
-
269
203
  def symbol_to_hyphen_case(sym)
270
204
  sym.to_s.tr("_", "-")
271
205
  end