secure_headers 5.2.0 → 6.0.0.alpha01

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of secure_headers might be problematic. Click here for more details.

Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +8 -4
  3. data/CHANGELOG.md +3 -7
  4. data/Gemfile +1 -1
  5. data/docs/upgrading-to-6-0.md +50 -0
  6. data/lib/secure_headers.rb +14 -76
  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 +4 -17
  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 +12 -11
  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/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 -22
  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: 1b901bf6e9f4127c81ec207e46599054feb18f32
4
- data.tar.gz: 642585cfc04901c83ff5d90ea1600f5ef141fec4
3
+ metadata.gz: cd74fc8e9c9057255c1e6c75bc78554ec6751459
4
+ data.tar.gz: 2f72e782c0800b5bf124d841a0d556d321b1cb5f
5
5
  SHA512:
6
- metadata.gz: 8e304a7d52e6dbf0a4fd2bb114201ba781e8565fd9ecbd4977e6169f922101f186c7024194c81ecd780376f2e4c18f7af3bd88f98cd2b86301b15de5e4148c44
7
- data.tar.gz: 9d5e1e8f35954baea9e4fcec70af22e7f3c2d8b16deaa98d6c882c9373c9a76556a711696b081920d9279df96375dd6e2a473e265c9bfbf2e12a6f9a62da486f
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,11 +1,7 @@
1
- ## 5.2.0
2
-
3
- Fixes newline injection issue
4
-
5
- ## 5.1.0
6
-
7
- Fixes semicolon injection issue reported by @mvgijssel see https://github.com/twitter/secure_headers/issues/418
1
+ ## 6.0
8
2
 
3
+ - See the [upgrading to 6.0](docs/upgrading-to-6-0.md) guide for the breaking changes.
4
+ =======
9
5
  ## 5.0.5
10
6
 
11
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.
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- require "secure_headers/configuration"
3
2
  require "secure_headers/hash_helper"
4
3
  require "secure_headers/headers/cookie"
5
4
  require "secure_headers/headers/public_key_pins"
@@ -18,6 +17,7 @@ require "secure_headers/railtie"
18
17
  require "secure_headers/view_helper"
19
18
  require "useragent"
20
19
  require "singleton"
20
+ require "secure_headers/configuration"
21
21
 
22
22
  # All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT
23
23
  # or ":optout_of_protection" as a config value to disable a given header
@@ -52,29 +52,9 @@ module SecureHeaders
52
52
  HTTPS = "https".freeze
53
53
  CSP = ContentSecurityPolicy
54
54
 
55
- ALL_HEADER_CLASSES = [
56
- ExpectCertificateTransparency,
57
- ClearSiteData,
58
- ContentSecurityPolicyConfig,
59
- ContentSecurityPolicyReportOnlyConfig,
60
- StrictTransportSecurity,
61
- PublicKeyPins,
62
- ReferrerPolicy,
63
- XContentTypeOptions,
64
- XDownloadOptions,
65
- XFrameOptions,
66
- XPermittedCrossDomainPolicies,
67
- XXssProtection
68
- ].freeze
69
-
70
- ALL_HEADERS_BESIDES_CSP = (
71
- ALL_HEADER_CLASSES -
72
- [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig]
73
- ).freeze
74
-
75
55
  # Headers set on http requests (excludes STS and HPKP)
76
- HTTP_HEADER_CLASSES =
77
- (ALL_HEADER_CLASSES - [StrictTransportSecurity, PublicKeyPins]).freeze
56
+ HTTPS_HEADER_CLASSES =
57
+ [StrictTransportSecurity, PublicKeyPins].freeze
78
58
 
79
59
  class << self
80
60
  # Public: override a given set of directives for the current request. If a
@@ -153,7 +133,7 @@ module SecureHeaders
153
133
  # Public: opts out of setting all headers by telling secure_headers to use
154
134
  # the NOOP configuration.
155
135
  def opt_out_of_all_protection(request)
156
- use_secure_headers_override(request, Configuration::NOOP_CONFIGURATION)
136
+ use_secure_headers_override(request, Configuration::NOOP_OVERRIDE)
157
137
  end
158
138
 
159
139
  # Public: Builds the hash of headers that should be applied base on the
@@ -168,27 +148,16 @@ module SecureHeaders
168
148
  def header_hash_for(request)
169
149
  prevent_dup = true
170
150
  config = config_for(request, prevent_dup)
171
- headers = config.cached_headers
151
+ config.validate_config!
172
152
  user_agent = UserAgent.parse(request.user_agent)
153
+ headers = config.generate_headers(user_agent)
173
154
 
174
- if !config.csp.opt_out? && config.csp.modified?
175
- headers = update_cached_csp(config.csp, headers, user_agent)
176
- end
177
-
178
- if !config.csp_report_only.opt_out? && config.csp_report_only.modified?
179
- headers = update_cached_csp(config.csp_report_only, headers, user_agent)
180
- end
181
-
182
- header_classes_for(request).each_with_object({}) do |klass, hash|
183
- if header = headers[klass::CONFIG_KEY]
184
- header_name, value = if klass == ContentSecurityPolicyConfig || klass == ContentSecurityPolicyReportOnlyConfig
185
- csp_header_for_ua(header, user_agent)
186
- else
187
- header
188
- end
189
- hash[header_name] = value
155
+ if request.scheme != HTTPS
156
+ HTTPS_HEADER_CLASSES.each do |klass|
157
+ headers.delete(klass::HEADER_NAME)
190
158
  end
191
159
  end
160
+ headers
192
161
  end
193
162
 
194
163
  # Public: specify which named override will be used for this request.
@@ -196,11 +165,9 @@ module SecureHeaders
196
165
  #
197
166
  # name - the name of the previously configured override.
198
167
  def use_secure_headers_override(request, name)
199
- if config = Configuration.get(name, internal: true)
200
- override_secure_headers_request_config(request, config)
201
- else
202
- raise ArgumentError.new("no override by the name of #{name} has been configured")
203
- end
168
+ config = config_for(request)
169
+ config.override(name)
170
+ override_secure_headers_request_config(request, config)
204
171
  end
205
172
 
206
173
  # Public: gets or creates a nonce for CSP.
@@ -228,7 +195,7 @@ module SecureHeaders
228
195
  # Falls back to the global config
229
196
  def config_for(request, prevent_dup = false)
230
197
  config = request.env[SECURE_HEADERS_CONFIG] ||
231
- Configuration.get(Configuration::DEFAULT_CONFIG, internal: true)
198
+ Configuration.send(:default_config)
232
199
 
233
200
 
234
201
  # Global configs are frozen, per-request configs are not. When we're not
@@ -285,35 +252,6 @@ module SecureHeaders
285
252
  def override_secure_headers_request_config(request, config)
286
253
  request.env[SECURE_HEADERS_CONFIG] = config
287
254
  end
288
-
289
- # Private: determines which headers are applicable to a given request.
290
- #
291
- # Returns a list of classes whose corresponding header values are valid for
292
- # this request.
293
- def header_classes_for(request)
294
- if request.scheme == HTTPS
295
- ALL_HEADER_CLASSES
296
- else
297
- HTTP_HEADER_CLASSES
298
- end
299
- end
300
-
301
- def update_cached_csp(config, headers, user_agent)
302
- headers = Configuration.send(:deep_copy, headers)
303
- headers[config.class::CONFIG_KEY] = {}
304
- variation = ContentSecurityPolicy.ua_to_variation(user_agent)
305
- headers[config.class::CONFIG_KEY][variation] = ContentSecurityPolicy.make_header(config, user_agent)
306
- headers
307
- end
308
-
309
- # Private: chooses the applicable CSP header for the provided user agent.
310
- #
311
- # headers - a hash of header_config_key => [header_name, header_value]
312
- #
313
- # Returns a CSP [header, value] array
314
- def csp_header_for_ua(headers, user_agent)
315
- headers[ContentSecurityPolicy.ua_to_variation(user_agent)]
316
- end
317
255
  end
318
256
 
319
257
  # These methods are mixed into controllers and delegate to the class method
@@ -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