secure_headers 3.2.0 → 3.8.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 (53) hide show
  1. checksums.yaml +5 -5
  2. data/.github/ISSUE_TEMPLATE.md +41 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +20 -0
  4. data/.rspec +1 -0
  5. data/.ruby-version +1 -1
  6. data/.travis.yml +2 -0
  7. data/CHANGELOG.md +160 -1
  8. data/CODE_OF_CONDUCT.md +46 -0
  9. data/CONTRIBUTING.md +41 -0
  10. data/Gemfile +3 -1
  11. data/LICENSE +4 -199
  12. data/README.md +75 -334
  13. data/docs/HPKP.md +17 -0
  14. data/docs/cookies.md +51 -0
  15. data/docs/hashes.md +64 -0
  16. data/docs/named_overrides_and_appends.md +107 -0
  17. data/docs/per_action_configuration.md +105 -0
  18. data/docs/sinatra.md +25 -0
  19. data/lib/secure_headers/configuration.rb +120 -39
  20. data/lib/secure_headers/headers/clear_site_data.rb +54 -0
  21. data/lib/secure_headers/headers/content_security_policy.rb +134 -36
  22. data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
  23. data/lib/secure_headers/headers/cookie.rb +7 -1
  24. data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
  25. data/lib/secure_headers/headers/policy_management.rb +150 -66
  26. data/lib/secure_headers/headers/public_key_pins.rb +1 -1
  27. data/lib/secure_headers/headers/referrer_policy.rb +36 -0
  28. data/lib/secure_headers/headers/strict_transport_security.rb +1 -1
  29. data/lib/secure_headers/headers/x_content_type_options.rb +1 -1
  30. data/lib/secure_headers/headers/x_download_options.rb +1 -1
  31. data/lib/secure_headers/headers/x_frame_options.rb +1 -1
  32. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +1 -1
  33. data/lib/secure_headers/headers/x_xss_protection.rb +1 -1
  34. data/lib/secure_headers/middleware.rb +6 -0
  35. data/lib/secure_headers/railtie.rb +1 -1
  36. data/lib/secure_headers/utils/cookies_config.rb +6 -4
  37. data/lib/secure_headers/view_helper.rb +13 -7
  38. data/lib/secure_headers.rb +138 -55
  39. data/lib/tasks/tasks.rake +5 -4
  40. data/secure_headers.gemspec +2 -2
  41. data/spec/lib/secure_headers/configuration_spec.rb +5 -5
  42. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +86 -0
  43. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +96 -13
  44. data/spec/lib/secure_headers/headers/cookie_spec.rb +22 -25
  45. data/spec/lib/secure_headers/headers/expect_certificate_transparency_spec.rb +42 -0
  46. data/spec/lib/secure_headers/headers/policy_management_spec.rb +59 -36
  47. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +72 -0
  48. data/spec/lib/secure_headers/middleware_spec.rb +31 -1
  49. data/spec/lib/secure_headers/view_helpers_spec.rb +19 -7
  50. data/spec/lib/secure_headers_spec.rb +344 -44
  51. data/spec/spec_helper.rb +8 -3
  52. data/upgrading-to-3-0.md +13 -10
  53. metadata +24 -5
@@ -0,0 +1,107 @@
1
+ ## Named Appends
2
+
3
+ Named Appends are blocks of code that can be reused and composed during requests. e.g. If a certain partial is rendered conditionally, and the csp needs to be adjusted for that partial, you can create a named append for that situation. The value returned by the block will be passed into `append_content_security_policy_directives`. The current request object is passed as an argument to the block for even more flexibility.
4
+
5
+ ```ruby
6
+ def show
7
+ if include_widget?
8
+ @widget = widget.render
9
+ use_content_security_policy_named_append(:widget_partial)
10
+ end
11
+ end
12
+
13
+
14
+ SecureHeaders::Configuration.named_append(:widget_partial) do |request|
15
+ SecureHeaders.override_x_frame_options(request, "DENY")
16
+ if request.controller_instance.current_user.in_test_bucket?
17
+ { child_src: %w(beta.thirdpartyhost.com) }
18
+ else
19
+ { child_src: %w(thirdpartyhost.com) }
20
+ end
21
+ end
22
+ ```
23
+
24
+ You can use as many named appends as you would like per request, but be careful because order of inclusion matters. Consider the following:
25
+
26
+ ```ruby
27
+ SecureHeader::Configuration.default do |config|
28
+ config.csp = { default_src: %w('self')}
29
+ end
30
+
31
+ SecureHeaders::Configuration.named_append(:A) do |request|
32
+ { default_src: %w(myhost.com) }
33
+ end
34
+
35
+ SecureHeaders::Configuration.named_append(:B) do |request|
36
+ { script_src: %w('unsafe-eval') }
37
+ end
38
+ ```
39
+
40
+ The following code will produce different policies due to the way policies are normalized (e.g. providing a previously undefined directive that inherits from `default-src`, removing host source values when `*` is provided. Removing `'none'` when additional values are present, etc.):
41
+
42
+ ```ruby
43
+ def index
44
+ use_content_security_policy_named_append(:A)
45
+ use_content_security_policy_named_append(:B)
46
+ # produces default-src 'self' myhost.com; script-src 'self' myhost.com 'unsafe-eval';
47
+ end
48
+
49
+ def show
50
+ use_content_security_policy_named_append(:B)
51
+ use_content_security_policy_named_append(:A)
52
+ # produces default-src 'self' myhost.com; script-src 'self' 'unsafe-eval';
53
+ end
54
+ ```
55
+
56
+
57
+ ## Named overrides
58
+
59
+ Named overrides serve two purposes:
60
+
61
+ * To be able to refer to a configuration by simple name.
62
+ * By precomputing the headers for a named configuration, the headers generated once and reused over every request.
63
+
64
+ To use a named override, drop a `SecureHeaders::Configuration.override` block **outside** of method definitions and then declare which named override you'd like to use. You can even override an override.
65
+
66
+ ```ruby
67
+ class ApplicationController < ActionController::Base
68
+ SecureHeaders::Configuration.default do |config|
69
+ config.csp = {
70
+ default_src: %w('self'),
71
+ script_src: %w(example.org)
72
+ }
73
+ end
74
+
75
+ # override default configuration
76
+ SecureHeaders::Configuration.override(:script_from_otherdomain_com) do |config|
77
+ config.csp[:script_src] << "otherdomain.com"
78
+ end
79
+
80
+ # overrides the :script_from_otherdomain_com configuration
81
+ SecureHeaders::Configuration.override(:another_config, :script_from_otherdomain_com) do |config|
82
+ config.csp[:script_src] << "evenanotherdomain.com"
83
+ end
84
+ end
85
+
86
+ class MyController < ApplicationController
87
+ def index
88
+ # Produces default-src 'self'; script-src example.org otherdomain.com
89
+ use_secure_headers_override(:script_from_otherdomain_com)
90
+ end
91
+
92
+ def show
93
+ # Produces default-src 'self'; script-src example.org otherdomain.org evenanotherdomain.com
94
+ use_secure_headers_override(:another_config)
95
+ end
96
+ end
97
+ ```
98
+
99
+ By default, a no-op configuration is provided. No headers will be set when this default override is used.
100
+
101
+ ```ruby
102
+ class MyController < ApplicationController
103
+ def index
104
+ SecureHeaders.opt_out_of_all_protection(request)
105
+ end
106
+ end
107
+ ```
@@ -0,0 +1,105 @@
1
+ ## Per-action configuration
2
+
3
+ You can override the settings for a given action by producing a temporary override. Be aware that because of the dynamic nature of the value, the header values will be computed per request.
4
+
5
+ ```ruby
6
+ # Given a config of:
7
+ ::SecureHeaders::Configuration.default do |config|
8
+ config.csp = {
9
+ default_src: %w('self'),
10
+ script_src: %w('self')
11
+ }
12
+ end
13
+
14
+ class MyController < ApplicationController
15
+ def index
16
+ # Append value to the source list, override 'none' values
17
+ # Produces: default-src 'self'; script-src 'self' s3.amazonaws.com; object-src 'self' www.youtube.com
18
+ append_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self' www.youtube.com))
19
+
20
+ # Overrides the previously set source list, override 'none' values
21
+ # Produces: default-src 'self'; script-src s3.amazonaws.com; object-src 'self'
22
+ override_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self'))
23
+
24
+ # Global settings default to "sameorigin"
25
+ override_x_frame_options("DENY")
26
+ end
27
+ ```
28
+
29
+ The following methods are available as controller instance methods. They are also available as class methods, but require you to pass in the `request` object.
30
+ * `append_content_security_policy_directives(hash)`: appends each value to the corresponding CSP app-wide configuration.
31
+ * `override_content_security_policy_directives(hash)`: merges the hash into the app-wide configuration, overwriting any previous config
32
+ * `override_x_frame_options(value)`: sets the `X-Frame-Options header` to `value`
33
+
34
+ ## Appending / overriding Content Security Policy
35
+
36
+ When manipulating content security policy, there are a few things to consider. The default header value is `default-src https:` which corresponds to a default configuration of `{ default_src: %w(https:)}`.
37
+
38
+ #### Append to the policy with a directive other than `default_src`
39
+
40
+ The value of `default_src` is joined with the addition if the it is a [fetch directive](https://w3c.github.io/webappsec-csp/#directives-fetch). Note the `https:` is carried over from the `default-src` config. If you do not want this, use `override_content_security_policy_directives` instead. To illustrate:
41
+
42
+ ```ruby
43
+ ::SecureHeaders::Configuration.default do |config|
44
+ config.csp = {
45
+ default_src: %w('self')
46
+ }
47
+ end
48
+ ```
49
+
50
+ Code | Result
51
+ ------------- | -------------
52
+ `append_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src 'self' mycdn.com`
53
+ `override_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src mycdn.com`
54
+
55
+ #### Nonce
56
+
57
+ You can use a view helper to automatically add nonces to script tags:
58
+
59
+ ```erb
60
+ <%= nonced_javascript_tag do %>
61
+ console.log("nonced!");
62
+ <% end %>
63
+
64
+ <%= nonced_style_tag do %>
65
+ body {
66
+ background-color: black;
67
+ }
68
+ <% end %>
69
+ ```
70
+
71
+ becomes:
72
+
73
+ ```html
74
+ <script nonce="/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=">
75
+ console.log("nonced!")
76
+ </script>
77
+ <style nonce="/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=">
78
+ body {
79
+ background-color: black;
80
+ }
81
+ </style>
82
+ ```
83
+
84
+ ```
85
+
86
+ Content-Security-Policy: ...
87
+ script-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...;
88
+ style-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...;
89
+ ```
90
+
91
+ `script`/`style-nonce` can be used to whitelist inline content. To do this, call the `content_security_policy_script_nonce` or `content_security_policy_style_nonce` then set the nonce attributes on the various tags.
92
+
93
+ ```erb
94
+ <script nonce="<%= content_security_policy_script_nonce %>">
95
+ console.log("whitelisted, will execute")
96
+ </script>
97
+
98
+ <script nonce="lol">
99
+ console.log("won't execute, not whitelisted")
100
+ </script>
101
+
102
+ <script>
103
+ console.log("won't execute, not whitelisted")
104
+ </script>
105
+ ```
data/docs/sinatra.md ADDED
@@ -0,0 +1,25 @@
1
+ ## Sinatra
2
+
3
+ Here's an example using SecureHeaders for Sinatra applications:
4
+
5
+ ```ruby
6
+ require 'rubygems'
7
+ require 'sinatra'
8
+ require 'haml'
9
+ require 'secure_headers'
10
+
11
+ use SecureHeaders::Middleware
12
+
13
+ SecureHeaders::Configuration.default do |config|
14
+ ...
15
+ end
16
+
17
+ class Donkey < Sinatra::Application
18
+ set :root, APP_ROOT
19
+
20
+ get '/' do
21
+ SecureHeaders.override_x_frame_options(request, SecureHeaders::OPT_OUT)
22
+ haml :index
23
+ end
24
+ end
25
+ ```
@@ -31,7 +31,7 @@ module SecureHeaders
31
31
  raise NotYetConfiguredError, "#{base} policy not yet supplied"
32
32
  end
33
33
  override = @configurations[base].dup
34
- override.instance_eval &block if block_given?
34
+ override.instance_eval(&block) if block_given?
35
35
  add_configuration(name, override)
36
36
  end
37
37
 
@@ -46,6 +46,17 @@ module SecureHeaders
46
46
  @configurations[name]
47
47
  end
48
48
 
49
+ def named_appends(name)
50
+ @appends ||= {}
51
+ @appends[name]
52
+ end
53
+
54
+ def named_append(name, target = nil, &block)
55
+ @appends ||= {}
56
+ raise "Provide a configuration block" unless block_given?
57
+ @appends[name] = block
58
+ end
59
+
49
60
  private
50
61
 
51
62
  # Private: add a valid configuration to the global set of named configs.
@@ -61,6 +72,7 @@ module SecureHeaders
61
72
  config.validate_config!
62
73
  @configurations ||= {}
63
74
  config.send(:cache_headers!)
75
+ config.send(:cache_hpkp_report_host)
64
76
  config.freeze
65
77
  @configurations[name] = config
66
78
  end
@@ -73,7 +85,6 @@ module SecureHeaders
73
85
  ALL_HEADER_CLASSES.each do |klass|
74
86
  config.send("#{klass::CONFIG_KEY}=", OPT_OUT)
75
87
  end
76
- config.dynamic_csp = OPT_OUT
77
88
  end
78
89
 
79
90
  add_configuration(NOOP_CONFIGURATION, noop_config)
@@ -82,6 +93,7 @@ module SecureHeaders
82
93
  # Public: perform a basic deep dup. The shallow copy provided by dup/clone
83
94
  # can lead to modifying parent objects.
84
95
  def deep_copy(config)
96
+ return unless config
85
97
  config.each_with_object({}) do |(key, value), hash|
86
98
  hash[key] = if value.is_a?(Array)
87
99
  value.dup
@@ -104,21 +116,41 @@ module SecureHeaders
104
116
 
105
117
  attr_writer :hsts, :x_frame_options, :x_content_type_options,
106
118
  :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
107
- :hpkp, :dynamic_csp, :cookies
119
+ :referrer_policy, :clear_site_data, :expect_certificate_transparency
120
+
121
+ attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host
108
122
 
109
- attr_reader :cached_headers, :csp, :dynamic_csp, :cookies
123
+ @script_hashes = nil
124
+ @style_hashes = nil
110
125
 
111
126
  HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml"
112
- if File.exists?(HASH_CONFIG_FILE)
127
+ if File.exist?(HASH_CONFIG_FILE)
113
128
  config = YAML.safe_load(File.open(HASH_CONFIG_FILE))
114
129
  @script_hashes = config["scripts"]
115
130
  @style_hashes = config["styles"]
116
131
  end
117
132
 
118
133
  def initialize(&block)
134
+ @cookies = nil
135
+ @clear_site_data = nil
136
+ @csp = nil
137
+ @csp_report_only = nil
138
+ @hpkp_report_host = nil
139
+ @hpkp = nil
140
+ @hsts = nil
141
+ @x_content_type_options = nil
142
+ @x_download_options = nil
143
+ @x_frame_options = nil
144
+ @x_permitted_cross_domain_policies = nil
145
+ @x_xss_protection = nil
146
+ @expect_certificate_transparency = nil
147
+
119
148
  self.hpkp = OPT_OUT
120
- self.csp = self.class.send(:deep_copy, CSP::DEFAULT_CONFIG)
121
- instance_eval &block if block_given?
149
+ self.referrer_policy = OPT_OUT
150
+ self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT)
151
+ self.csp_report_only = OPT_OUT
152
+
153
+ instance_eval(&block) if block_given?
122
154
  end
123
155
 
124
156
  # Public: copy everything but the cached headers
@@ -126,9 +158,9 @@ module SecureHeaders
126
158
  # Returns a deep-dup'd copy of this configuration.
127
159
  def dup
128
160
  copy = self.class.new
129
- copy.cookies = @cookies
130
- copy.csp = self.class.send(:deep_copy_if_hash, @csp)
131
- copy.dynamic_csp = self.class.send(:deep_copy_if_hash, @dynamic_csp)
161
+ copy.cookies = self.class.send(:deep_copy_if_hash, @cookies)
162
+ copy.csp = @csp.dup if @csp
163
+ copy.csp_report_only = @csp_report_only.dup if @csp_report_only
132
164
  copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
133
165
  copy.x_content_type_options = @x_content_type_options
134
166
  copy.hsts = @hsts
@@ -136,15 +168,16 @@ module SecureHeaders
136
168
  copy.x_xss_protection = @x_xss_protection
137
169
  copy.x_download_options = @x_download_options
138
170
  copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies
171
+ copy.clear_site_data = @clear_site_data
172
+ copy.expect_certificate_transparency = @expect_certificate_transparency
173
+ copy.referrer_policy = @referrer_policy
139
174
  copy.hpkp = @hpkp
175
+ copy.hpkp_report_host = @hpkp_report_host
140
176
  copy
141
177
  end
142
178
 
143
179
  def opt_out(header)
144
180
  send("#{header}=", OPT_OUT)
145
- if header == CSP::CONFIG_KEY
146
- dynamic_csp = OPT_OUT
147
- end
148
181
  self.cached_headers.delete(header)
149
182
  end
150
183
 
@@ -153,20 +186,6 @@ module SecureHeaders
153
186
  self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value)
154
187
  end
155
188
 
156
- # Public: generated cached headers for a specific user agent.
157
- def rebuild_csp_header_cache!(user_agent)
158
- self.cached_headers[CSP::CONFIG_KEY] = {}
159
- unless current_csp == OPT_OUT
160
- user_agent = UserAgent.parse(user_agent)
161
- variation = CSP.ua_to_variation(user_agent)
162
- self.cached_headers[CSP::CONFIG_KEY][variation] = CSP.make_header(current_csp, user_agent)
163
- end
164
- end
165
-
166
- def current_csp
167
- @dynamic_csp || @csp
168
- end
169
-
170
189
  # Public: validates all configurations values.
171
190
  #
172
191
  # Raises various configuration errors if any invalid config is detected.
@@ -175,11 +194,15 @@ module SecureHeaders
175
194
  def validate_config!
176
195
  StrictTransportSecurity.validate_config!(@hsts)
177
196
  ContentSecurityPolicy.validate_config!(@csp)
197
+ ContentSecurityPolicy.validate_config!(@csp_report_only)
198
+ ReferrerPolicy.validate_config!(@referrer_policy)
178
199
  XFrameOptions.validate_config!(@x_frame_options)
179
200
  XContentTypeOptions.validate_config!(@x_content_type_options)
180
201
  XXssProtection.validate_config!(@x_xss_protection)
181
202
  XDownloadOptions.validate_config!(@x_download_options)
182
203
  XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
204
+ ClearSiteData.validate_config!(@clear_site_data)
205
+ ExpectCertificateTransparency.validate_config!(@expect_certificate_transparency)
183
206
  PublicKeyPins.validate_config!(@hpkp)
184
207
  Cookie.validate_config!(@cookies)
185
208
  end
@@ -189,23 +212,77 @@ module SecureHeaders
189
212
  @cookies = (@cookies || {}).merge(secure: secure_cookies)
190
213
  end
191
214
 
192
- protected
193
-
194
215
  def csp=(new_csp)
195
- if self.dynamic_csp
196
- raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= instead."
216
+ if new_csp.respond_to?(:opt_out?)
217
+ @csp = new_csp.dup
218
+ else
219
+ if new_csp[:report_only]
220
+ # Deprecated configuration implies that CSPRO should be set, CSP should not - so opt out
221
+ Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp=` was supplied a config with report_only: true. Use #csp_report_only="
222
+ @csp = OPT_OUT
223
+ self.csp_report_only = new_csp
224
+ else
225
+ @csp = ContentSecurityPolicyConfig.new(new_csp)
226
+ end
197
227
  end
228
+ end
198
229
 
199
- @csp = new_csp
230
+ # Configures the Content-Security-Policy-Report-Only header. `new_csp` cannot
231
+ # contain `report_only: false` or an error will be raised.
232
+ #
233
+ # NOTE: if csp has not been configured/has the default value when
234
+ # configuring csp_report_only, the code will assume you mean to only use
235
+ # report-only mode and you will be opted-out of enforce mode.
236
+ def csp_report_only=(new_csp)
237
+ @csp_report_only = begin
238
+ if new_csp.is_a?(ContentSecurityPolicyConfig)
239
+ new_csp.make_report_only
240
+ elsif new_csp.respond_to?(:opt_out?)
241
+ new_csp.dup
242
+ else
243
+ if new_csp[:report_only] == false # nil is a valid value on which we do not want to raise
244
+ raise ContentSecurityPolicyConfigError, "`#csp_report_only=` was supplied a config with report_only: false. Use #csp="
245
+ else
246
+ ContentSecurityPolicyReportOnlyConfig.new(new_csp)
247
+ end
248
+ end
249
+ end
250
+
251
+ if !@csp_report_only.opt_out? && @csp.to_h == ContentSecurityPolicyConfig::DEFAULT
252
+ Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp_report_only=` was configured before `#csp=`. It is assumed you intended to opt out of `#csp=` so be sure to add `config.csp = SecureHeaders::OPT_OUT` to your config. Ensure that #csp_report_only is configured after #csp="
253
+ @csp = OPT_OUT
254
+ end
255
+ end
256
+
257
+ protected
258
+
259
+ def cookies=(cookies)
260
+ @cookies = cookies
200
261
  end
201
262
 
202
263
  def cached_headers=(headers)
203
264
  @cached_headers = headers
204
265
  end
205
266
 
267
+ def hpkp=(hpkp)
268
+ @hpkp = self.class.send(:deep_copy_if_hash, hpkp)
269
+ end
270
+
271
+ def hpkp_report_host=(hpkp_report_host)
272
+ @hpkp_report_host = hpkp_report_host
273
+ end
274
+
206
275
  private
207
276
 
208
- # Public: Precompute the header names and values for this configuraiton.
277
+ def cache_hpkp_report_host
278
+ has_report_uri = @hpkp && @hpkp != OPT_OUT && @hpkp[:report_uri]
279
+ self.hpkp_report_host = if has_report_uri
280
+ parsed_report_uri = URI.parse(@hpkp[:report_uri])
281
+ parsed_report_uri.host
282
+ end
283
+ end
284
+
285
+ # Public: Precompute the header names and values for this configuration.
209
286
  # Ensures that headers generated at configure time, not on demand.
210
287
  #
211
288
  # Returns the cached headers
@@ -231,12 +308,16 @@ module SecureHeaders
231
308
  #
232
309
  # Returns nothing
233
310
  def generate_csp_headers(headers)
234
- unless @csp == OPT_OUT
235
- headers[CSP::CONFIG_KEY] = {}
236
- csp_config = self.current_csp
237
- CSP::VARIATIONS.each do |name, _|
238
- csp = CSP.make_header(csp_config, UserAgent.parse(name))
239
- headers[CSP::CONFIG_KEY][name] = csp.freeze
311
+ generate_csp_headers_for_config(headers, ContentSecurityPolicyConfig::CONFIG_KEY, self.csp)
312
+ generate_csp_headers_for_config(headers, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY, self.csp_report_only)
313
+ end
314
+
315
+ def generate_csp_headers_for_config(headers, header_key, csp_config)
316
+ unless csp_config.opt_out?
317
+ headers[header_key] = {}
318
+ ContentSecurityPolicy::VARIATIONS.each do |name, _|
319
+ csp = ContentSecurityPolicy.make_header(csp_config, UserAgent.parse(name))
320
+ headers[header_key][name] = csp.freeze
240
321
  end
241
322
  end
242
323
  end
@@ -0,0 +1,54 @@
1
+ module SecureHeaders
2
+ class ClearSiteDataConfigError < StandardError; end
3
+ class ClearSiteData
4
+ HEADER_NAME = "Clear-Site-Data".freeze
5
+
6
+ # Valid `types`
7
+ CACHE = "cache".freeze
8
+ COOKIES = "cookies".freeze
9
+ STORAGE = "storage".freeze
10
+ EXECTION_CONTEXTS = "executionContexts".freeze
11
+ ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECTION_CONTEXTS]
12
+
13
+ CONFIG_KEY = :clear_site_data
14
+
15
+ class << self
16
+ # Public: make an Clear-Site-Data header name, value pair
17
+ #
18
+ # Returns nil if not configured, returns header name and value if configured.
19
+ def make_header(config=nil)
20
+ case config
21
+ when nil, OPT_OUT, []
22
+ # noop
23
+ when Array
24
+ [HEADER_NAME, make_header_value(config)]
25
+ when true
26
+ [HEADER_NAME, make_header_value(ALL_TYPES)]
27
+ end
28
+ end
29
+
30
+ def validate_config!(config)
31
+ case config
32
+ when nil, OPT_OUT, true
33
+ # valid
34
+ when Array
35
+ unless config.all? { |t| t.is_a?(String) }
36
+ raise ClearSiteDataConfigError.new("types must be Strings")
37
+ end
38
+ else
39
+ raise ClearSiteDataConfigError.new("config must be an Array of Strings or `true`")
40
+ end
41
+ end
42
+
43
+ # Public: Transform a Clear-Site-Data config (an Array of Strings) into a
44
+ # String that can be used as the value for the Clear-Site-Data header.
45
+ #
46
+ # types - An Array of String of types of data to clear.
47
+ #
48
+ # Returns a String of quoted values that are comma separated.
49
+ def make_header_value(types)
50
+ types.map { |t| "\"#{t}\""}.join(", ")
51
+ end
52
+ end
53
+ end
54
+ end