secure_headers 3.4.0 → 4.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +2 -0
  3. data/.rubocop.yml +3 -0
  4. data/.ruby-version +1 -1
  5. data/.travis.yml +8 -4
  6. data/CHANGELOG.md +113 -1
  7. data/CODE_OF_CONDUCT.md +46 -0
  8. data/CONTRIBUTING.md +41 -0
  9. data/Gemfile +8 -4
  10. data/Guardfile +1 -0
  11. data/LICENSE +4 -199
  12. data/README.md +63 -333
  13. data/Rakefile +22 -18
  14. data/docs/HPKP.md +17 -0
  15. data/docs/cookies.md +64 -0
  16. data/docs/hashes.md +64 -0
  17. data/docs/named_overrides_and_appends.md +107 -0
  18. data/docs/per_action_configuration.md +133 -0
  19. data/docs/sinatra.md +25 -0
  20. data/lib/secure_headers/configuration.rb +91 -44
  21. data/lib/secure_headers/hash_helper.rb +2 -1
  22. data/lib/secure_headers/headers/clear_site_data.rb +55 -0
  23. data/lib/secure_headers/headers/content_security_policy.rb +110 -51
  24. data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
  25. data/lib/secure_headers/headers/cookie.rb +21 -3
  26. data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
  27. data/lib/secure_headers/headers/policy_management.rb +143 -69
  28. data/lib/secure_headers/headers/public_key_pins.rb +5 -4
  29. data/lib/secure_headers/headers/referrer_policy.rb +5 -1
  30. data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
  31. data/lib/secure_headers/headers/x_content_type_options.rb +2 -1
  32. data/lib/secure_headers/headers/x_download_options.rb +3 -2
  33. data/lib/secure_headers/headers/x_frame_options.rb +2 -1
  34. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +3 -2
  35. data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
  36. data/lib/secure_headers/middleware.rb +10 -9
  37. data/lib/secure_headers/railtie.rb +7 -6
  38. data/lib/secure_headers/utils/cookies_config.rb +13 -12
  39. data/lib/secure_headers/view_helper.rb +12 -3
  40. data/lib/secure_headers.rb +137 -55
  41. data/lib/tasks/tasks.rake +2 -1
  42. data/secure_headers.gemspec +13 -3
  43. data/spec/lib/secure_headers/configuration_spec.rb +13 -12
  44. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +87 -0
  45. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +59 -26
  46. data/spec/lib/secure_headers/headers/cookie_spec.rb +38 -20
  47. data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
  48. data/spec/lib/secure_headers/headers/policy_management_spec.rb +78 -40
  49. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
  50. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +22 -3
  51. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +5 -4
  52. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
  53. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
  54. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
  55. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
  56. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
  57. data/spec/lib/secure_headers/middleware_spec.rb +39 -19
  58. data/spec/lib/secure_headers/view_helpers_spec.rb +21 -8
  59. data/spec/lib/secure_headers_spec.rb +304 -56
  60. data/spec/spec_helper.rb +13 -24
  61. data/upgrading-to-4-0.md +55 -0
  62. metadata +29 -7
@@ -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,133 @@
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
+ ```
106
+
107
+ ## Clearing browser cache
108
+
109
+ You can clear the browser cache after the logout request by using the following.
110
+
111
+ ``` ruby
112
+ class ApplicationController < ActionController::Base
113
+ # Configuration override to send the Clear-Site-Data header.
114
+ SecureHeaders::Configuration.override(:clear_browser_cache) do |config|
115
+ config.clear_site_data = [
116
+ SecureHeaders::ClearSiteData::ALL_TYPES
117
+ ]
118
+ end
119
+
120
+
121
+ # Clears the browser's cache for browsers supporting the Clear-Site-Data
122
+ # header.
123
+ #
124
+ # Returns nothing.
125
+ def clear_browser_cache
126
+ SecureHeaders.use_secure_headers_override(request, :clear_browser_cache)
127
+ end
128
+ end
129
+
130
+ class SessionsController < ApplicationController
131
+ after_action :clear_browser_cache, only: :destroy
132
+ end
133
+ ```
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
+ ```
@@ -1,4 +1,5 @@
1
- require 'yaml'
1
+ # frozen_string_literal: true
2
+ require "yaml"
2
3
 
3
4
  module SecureHeaders
4
5
  class Configuration
@@ -31,7 +32,7 @@ module SecureHeaders
31
32
  raise NotYetConfiguredError, "#{base} policy not yet supplied"
32
33
  end
33
34
  override = @configurations[base].dup
34
- override.instance_eval &block if block_given?
35
+ override.instance_eval(&block) if block_given?
35
36
  add_configuration(name, override)
36
37
  end
37
38
 
@@ -46,6 +47,17 @@ module SecureHeaders
46
47
  @configurations[name]
47
48
  end
48
49
 
50
+ def named_appends(name)
51
+ @appends ||= {}
52
+ @appends[name]
53
+ end
54
+
55
+ def named_append(name, target = nil, &block)
56
+ @appends ||= {}
57
+ raise "Provide a configuration block" unless block_given?
58
+ @appends[name] = block
59
+ end
60
+
49
61
  private
50
62
 
51
63
  # Private: add a valid configuration to the global set of named configs.
@@ -74,7 +86,6 @@ module SecureHeaders
74
86
  ALL_HEADER_CLASSES.each do |klass|
75
87
  config.send("#{klass::CONFIG_KEY}=", OPT_OUT)
76
88
  end
77
- config.dynamic_csp = OPT_OUT
78
89
  end
79
90
 
80
91
  add_configuration(NOOP_CONFIGURATION, noop_config)
@@ -83,6 +94,7 @@ module SecureHeaders
83
94
  # Public: perform a basic deep dup. The shallow copy provided by dup/clone
84
95
  # can lead to modifying parent objects.
85
96
  def deep_copy(config)
97
+ return unless config
86
98
  config.each_with_object({}) do |(key, value), hash|
87
99
  hash[key] = if value.is_a?(Array)
88
100
  value.dup
@@ -103,26 +115,43 @@ module SecureHeaders
103
115
  end
104
116
  end
105
117
 
106
- attr_accessor :dynamic_csp
107
-
108
118
  attr_writer :hsts, :x_frame_options, :x_content_type_options,
109
119
  :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
110
- :referrer_policy
120
+ :referrer_policy, :clear_site_data, :expect_certificate_transparency
121
+
122
+ attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host
111
123
 
112
- attr_reader :cached_headers, :csp, :cookies, :hpkp, :hpkp_report_host
124
+ @script_hashes = nil
125
+ @style_hashes = nil
113
126
 
114
127
  HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml"
115
- if File.exists?(HASH_CONFIG_FILE)
128
+ if File.exist?(HASH_CONFIG_FILE)
116
129
  config = YAML.safe_load(File.open(HASH_CONFIG_FILE))
117
130
  @script_hashes = config["scripts"]
118
131
  @style_hashes = config["styles"]
119
132
  end
120
133
 
121
134
  def initialize(&block)
135
+ @cookies = nil
136
+ @clear_site_data = nil
137
+ @csp = nil
138
+ @csp_report_only = nil
139
+ @hpkp_report_host = nil
140
+ @hpkp = nil
141
+ @hsts = nil
142
+ @x_content_type_options = nil
143
+ @x_download_options = nil
144
+ @x_frame_options = nil
145
+ @x_permitted_cross_domain_policies = nil
146
+ @x_xss_protection = nil
147
+ @expect_certificate_transparency = nil
148
+
122
149
  self.hpkp = OPT_OUT
123
150
  self.referrer_policy = OPT_OUT
124
- self.csp = self.class.send(:deep_copy, CSP::DEFAULT_CONFIG)
125
- instance_eval &block if block_given?
151
+ self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT)
152
+ self.csp_report_only = OPT_OUT
153
+
154
+ instance_eval(&block) if block_given?
126
155
  end
127
156
 
128
157
  # Public: copy everything but the cached headers
@@ -130,9 +159,9 @@ module SecureHeaders
130
159
  # Returns a deep-dup'd copy of this configuration.
131
160
  def dup
132
161
  copy = self.class.new
133
- copy.cookies = @cookies
134
- copy.csp = self.class.send(:deep_copy_if_hash, @csp)
135
- copy.dynamic_csp = self.class.send(:deep_copy_if_hash, @dynamic_csp)
162
+ copy.cookies = self.class.send(:deep_copy_if_hash, @cookies)
163
+ copy.csp = @csp.dup if @csp
164
+ copy.csp_report_only = @csp_report_only.dup if @csp_report_only
136
165
  copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
137
166
  copy.x_content_type_options = @x_content_type_options
138
167
  copy.hsts = @hsts
@@ -140,6 +169,8 @@ module SecureHeaders
140
169
  copy.x_xss_protection = @x_xss_protection
141
170
  copy.x_download_options = @x_download_options
142
171
  copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies
172
+ copy.clear_site_data = @clear_site_data
173
+ copy.expect_certificate_transparency = @expect_certificate_transparency
143
174
  copy.referrer_policy = @referrer_policy
144
175
  copy.hpkp = @hpkp
145
176
  copy.hpkp_report_host = @hpkp_report_host
@@ -148,9 +179,6 @@ module SecureHeaders
148
179
 
149
180
  def opt_out(header)
150
181
  send("#{header}=", OPT_OUT)
151
- if header == CSP::CONFIG_KEY
152
- dynamic_csp = OPT_OUT
153
- end
154
182
  self.cached_headers.delete(header)
155
183
  end
156
184
 
@@ -159,20 +187,6 @@ module SecureHeaders
159
187
  self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value)
160
188
  end
161
189
 
162
- # Public: generated cached headers for a specific user agent.
163
- def rebuild_csp_header_cache!(user_agent)
164
- self.cached_headers[CSP::CONFIG_KEY] = {}
165
- unless current_csp == OPT_OUT
166
- user_agent = UserAgent.parse(user_agent)
167
- variation = CSP.ua_to_variation(user_agent)
168
- self.cached_headers[CSP::CONFIG_KEY][variation] = CSP.make_header(current_csp, user_agent)
169
- end
170
- end
171
-
172
- def current_csp
173
- @dynamic_csp || @csp
174
- end
175
-
176
190
  # Public: validates all configurations values.
177
191
  #
178
192
  # Raises various configuration errors if any invalid config is detected.
@@ -181,31 +195,60 @@ module SecureHeaders
181
195
  def validate_config!
182
196
  StrictTransportSecurity.validate_config!(@hsts)
183
197
  ContentSecurityPolicy.validate_config!(@csp)
198
+ ContentSecurityPolicy.validate_config!(@csp_report_only)
184
199
  ReferrerPolicy.validate_config!(@referrer_policy)
185
200
  XFrameOptions.validate_config!(@x_frame_options)
186
201
  XContentTypeOptions.validate_config!(@x_content_type_options)
187
202
  XXssProtection.validate_config!(@x_xss_protection)
188
203
  XDownloadOptions.validate_config!(@x_download_options)
189
204
  XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
205
+ ClearSiteData.validate_config!(@clear_site_data)
206
+ ExpectCertificateTransparency.validate_config!(@expect_certificate_transparency)
190
207
  PublicKeyPins.validate_config!(@hpkp)
191
208
  Cookie.validate_config!(@cookies)
192
209
  end
193
210
 
194
211
  def secure_cookies=(secure_cookies)
195
- Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#secure_cookies=` is deprecated. Please use `#cookies=` to configure secure cookies instead."
196
- @cookies = (@cookies || {}).merge(secure: secure_cookies)
212
+ raise ArgumentError, "#{Kernel.caller.first}: `#secure_cookies=` is no longer supported. Please use `#cookies=` to configure secure cookies instead."
197
213
  end
198
214
 
199
- protected
200
-
201
215
  def csp=(new_csp)
202
- if self.dynamic_csp
203
- 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
+ # invalid configuration implies that CSPRO should be set, CSP should not - so opt out
221
+ raise ArgumentError, "#{Kernel.caller.first}: `#csp=` was supplied a config with report_only: true. Use #csp_report_only="
222
+ else
223
+ @csp = ContentSecurityPolicyConfig.new(new_csp)
224
+ end
204
225
  end
226
+ end
205
227
 
206
- @csp = self.class.send(:deep_copy_if_hash, new_csp)
228
+ # Configures the Content-Security-Policy-Report-Only header. `new_csp` cannot
229
+ # contain `report_only: false` or an error will be raised.
230
+ #
231
+ # NOTE: if csp has not been configured/has the default value when
232
+ # configuring csp_report_only, the code will assume you mean to only use
233
+ # report-only mode and you will be opted-out of enforce mode.
234
+ def csp_report_only=(new_csp)
235
+ @csp_report_only = begin
236
+ if new_csp.is_a?(ContentSecurityPolicyConfig)
237
+ new_csp.make_report_only
238
+ elsif new_csp.respond_to?(:opt_out?)
239
+ new_csp.dup
240
+ else
241
+ if new_csp[:report_only] == false # nil is a valid value on which we do not want to raise
242
+ raise ContentSecurityPolicyConfigError, "`#csp_report_only=` was supplied a config with report_only: false. Use #csp="
243
+ else
244
+ ContentSecurityPolicyReportOnlyConfig.new(new_csp)
245
+ end
246
+ end
247
+ end
207
248
  end
208
249
 
250
+ protected
251
+
209
252
  def cookies=(cookies)
210
253
  @cookies = cookies
211
254
  end
@@ -232,7 +275,7 @@ module SecureHeaders
232
275
  end
233
276
  end
234
277
 
235
- # Public: Precompute the header names and values for this configuraiton.
278
+ # Public: Precompute the header names and values for this configuration.
236
279
  # Ensures that headers generated at configure time, not on demand.
237
280
  #
238
281
  # Returns the cached headers
@@ -258,12 +301,16 @@ module SecureHeaders
258
301
  #
259
302
  # Returns nothing
260
303
  def generate_csp_headers(headers)
261
- unless @csp == OPT_OUT
262
- headers[CSP::CONFIG_KEY] = {}
263
- csp_config = self.current_csp
264
- CSP::VARIATIONS.each do |name, _|
265
- csp = CSP.make_header(csp_config, UserAgent.parse(name))
266
- headers[CSP::CONFIG_KEY][name] = csp.freeze
304
+ generate_csp_headers_for_config(headers, ContentSecurityPolicyConfig::CONFIG_KEY, self.csp)
305
+ generate_csp_headers_for_config(headers, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY, self.csp_report_only)
306
+ end
307
+
308
+ def generate_csp_headers_for_config(headers, header_key, csp_config)
309
+ unless csp_config.opt_out?
310
+ headers[header_key] = {}
311
+ ContentSecurityPolicy::VARIATIONS.each do |name, _|
312
+ csp = ContentSecurityPolicy.make_header(csp_config, UserAgent.parse(name))
313
+ headers[header_key][name] = csp.freeze
267
314
  end
268
315
  end
269
316
  end
@@ -1,4 +1,5 @@
1
- require 'base64'
1
+ # frozen_string_literal: true
2
+ require "base64"
2
3
 
3
4
  module SecureHeaders
4
5
  module HashHelper
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+ module SecureHeaders
3
+ class ClearSiteDataConfigError < StandardError; end
4
+ class ClearSiteData
5
+ HEADER_NAME = "Clear-Site-Data".freeze
6
+
7
+ # Valid `types`
8
+ CACHE = "cache".freeze
9
+ COOKIES = "cookies".freeze
10
+ STORAGE = "storage".freeze
11
+ EXECUTION_CONTEXTS = "executionContexts".freeze
12
+ ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECUTION_CONTEXTS]
13
+
14
+ CONFIG_KEY = :clear_site_data
15
+
16
+ class << self
17
+ # Public: make an Clear-Site-Data header name, value pair
18
+ #
19
+ # Returns nil if not configured, returns header name and value if configured.
20
+ def make_header(config = nil)
21
+ case config
22
+ when nil, OPT_OUT, []
23
+ # noop
24
+ when Array
25
+ [HEADER_NAME, make_header_value(config)]
26
+ when true
27
+ [HEADER_NAME, make_header_value(ALL_TYPES)]
28
+ end
29
+ end
30
+
31
+ def validate_config!(config)
32
+ case config
33
+ when nil, OPT_OUT, true
34
+ # valid
35
+ when Array
36
+ unless config.all? { |t| t.is_a?(String) }
37
+ raise ClearSiteDataConfigError.new("types must be Strings")
38
+ end
39
+ else
40
+ raise ClearSiteDataConfigError.new("config must be an Array of Strings or `true`")
41
+ end
42
+ end
43
+
44
+ # Public: Transform a Clear-Site-Data config (an Array of Strings) into a
45
+ # String that can be used as the value for the Clear-Site-Data header.
46
+ #
47
+ # types - An Array of String of types of data to clear.
48
+ #
49
+ # Returns a String of quoted values that are comma separated.
50
+ def make_header_value(types)
51
+ types.map { |t| "\"#{t}\""}.join(", ")
52
+ end
53
+ end
54
+ end
55
+ end