secure_headers 3.4.1 → 3.5.0.pre

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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 45e392304147a02bee8cf0709fe52e16f9ad255c
4
- data.tar.gz: e07030aa93c84a20e9b22d88af6489d8a71c95e7
3
+ metadata.gz: a34fab69fada104ff674198e9ee40271bcc86aea
4
+ data.tar.gz: c95c8b38f37b47dd66cf202c83fd4923b1aa3df9
5
5
  SHA512:
6
- metadata.gz: 8238f728eb74303b6aac54d40e3eaf1aacdaf7e0c910fe9a05f3dc005daa9eb96876391619eb2cb613a0d2a8ad358a48bc899626be46d201561f05064f47fdf8
7
- data.tar.gz: d0d448274a7a4789f668ac59c49903bf50889a2675a82d62d91ca721c5160b50288b5fafcf8b041422f575cc42d31b4a614fbc8036759fb522c149131a472f33
6
+ metadata.gz: 93d7bc9412154b01231352eb801f9a6a72438d6ce442dc70e8f11a7d013a466ab3fcadfdea6abda7642361e6baa509c1a4087da0630f5263d26c42b5c384a890
7
+ data.tar.gz: 403ac9a435812bfbbe7ccd4edeb96d646975ab769582680d19c58c48145609cb06ff1095a683e4f6853d74dd4688a7fb99f35f56147a583af6835b2126c957c3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 3.5.0.pre
2
+
3
+ This release adds support for setting two CSP headers (enforced/report-only) and management around them.
4
+
1
5
  ## 3.4.1 Named Appends
2
6
 
3
7
  ### Small bugfix
data/README.md CHANGED
@@ -36,7 +36,7 @@ SecureHeaders::Configuration.default do |config|
36
36
  secure: true, # mark all cookies as "Secure"
37
37
  httponly: true, # mark all cookies as "HttpOnly"
38
38
  samesite: {
39
- strict: true # mark all cookies as SameSite=Strict
39
+ lax: true # mark all cookies as SameSite=lax
40
40
  }
41
41
  }
42
42
  config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload"
@@ -48,7 +48,7 @@ SecureHeaders::Configuration.default do |config|
48
48
  config.referrer_policy = "origin-when-cross-origin"
49
49
  config.csp = {
50
50
  # "meta" values. these will shaped the header, but the values are not included in the header.
51
- report_only: true, # default: false
51
+ report_only: true, # default: false [DEPRECATED: instead, configure csp_report_only]
52
52
  preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
53
53
 
54
54
  # directive values: these values will directly translate into source directives
@@ -69,6 +69,10 @@ SecureHeaders::Configuration.default do |config|
69
69
  upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
70
70
  report_uri: %w(https://report-uri.io/example-csp)
71
71
  }
72
+ config.csp_report_only = config.csp.merge({
73
+ img_src: %w(somewhereelse.com),
74
+ report_uri: %w(https://report-uri.io/example-csp-report-only)
75
+ })
72
76
  config.hpkp = {
73
77
  report_only: false,
74
78
  max_age: 60.days.to_i,
@@ -92,7 +96,32 @@ use SecureHeaders::Middleware
92
96
 
93
97
  ## Default values
94
98
 
95
- All headers except for PublicKeyPins have a default value. See the [corresponding classes for their defaults](https://github.com/twitter/secureheaders/tree/master/lib/secure_headers/headers).
99
+ All headers except for PublicKeyPins have a default value. See the [corresponding classes for their defaults](https://github.com/twitter/secureheaders/tree/master/lib/secure_headers/headers). The default set of headers is:
100
+
101
+ ```
102
+ Content-Security-Policy: default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'
103
+ Strict-Transport-Security: max-age=631138519
104
+ X-Content-Type-Options: nosniff
105
+ X-Download-Options: noopen
106
+ X-Frame-Options: sameorigin
107
+ X-Permitted-Cross-Domain-Policies: none
108
+ X-Xss-Protection: 1; mode=block
109
+ ```
110
+
111
+ ### Default CSP
112
+
113
+ By default, the above CSP will be applied to all requests. If you **only** want to set a Report-Only header, opt-out of the default enforced header for clarity. The configuration will assume that if you only supply `csp_report_only` that you intended to opt-out of `csp` but that's for the sake of backwards compatibility and it will be removed in the future.
114
+
115
+ ```ruby
116
+ Configuration.default do |config|
117
+ config.csp = SecureHeaders::OPT_OUT # If this line is omitted, we will assume you meant to opt out.
118
+ config.csp_report_only = {
119
+ default_src: %w('self')
120
+ }
121
+ end
122
+ ```
123
+
124
+ If **
96
125
 
97
126
  ## Named Appends
98
127
 
@@ -14,18 +14,43 @@ require "secure_headers/middleware"
14
14
  require "secure_headers/railtie"
15
15
  require "secure_headers/view_helper"
16
16
  require "useragent"
17
+ require "singleton"
17
18
 
18
19
  # All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT
19
20
  # or ":optout_of_protection" as a config value to disable a given header
20
21
  module SecureHeaders
21
- OPT_OUT = :opt_out_of_protection
22
+ class NoOpHeaderConfig
23
+ include Singleton
24
+
25
+ def boom(arg = nil)
26
+ raise "Illegal State: attempted to modify NoOpHeaderConfig. Create a new config instead."
27
+ end
28
+
29
+ def to_h
30
+ {}
31
+ end
32
+
33
+ def dup
34
+ self.class.instance
35
+ end
36
+
37
+ def opt_out?
38
+ true
39
+ end
40
+
41
+ alias_method :[], :boom
42
+ alias_method :[]=, :boom
43
+ alias_method :keys, :boom
44
+ end
45
+
46
+ OPT_OUT = NoOpHeaderConfig.instance
22
47
  SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze
23
48
  NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze
24
49
  HTTPS = "https".freeze
25
- CSP = ContentSecurityPolicy
26
50
 
27
51
  ALL_HEADER_CLASSES = [
28
- ContentSecurityPolicy,
52
+ ContentSecurityPolicyConfig,
53
+ ContentSecurityPolicyReportOnlyConfig,
29
54
  StrictTransportSecurity,
30
55
  PublicKeyPins,
31
56
  ReferrerPolicy,
@@ -36,7 +61,10 @@ module SecureHeaders
36
61
  XXssProtection
37
62
  ].freeze
38
63
 
39
- ALL_HEADERS_BESIDES_CSP = (ALL_HEADER_CLASSES - [CSP]).freeze
64
+ ALL_HEADERS_BESIDES_CSP = (
65
+ ALL_HEADER_CLASSES -
66
+ [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig]
67
+ ).freeze
40
68
 
41
69
  # Headers set on http requests (excludes STS and HPKP)
42
70
  HTTP_HEADER_CLASSES =
@@ -50,13 +78,25 @@ module SecureHeaders
50
78
  #
51
79
  # additions - a hash containing directives. e.g.
52
80
  # script_src: %w(another-host.com)
53
- def override_content_security_policy_directives(request, additions)
54
- config = config_for(request)
55
- if config.current_csp == OPT_OUT
56
- config.dynamic_csp = {}
81
+ def override_content_security_policy_directives(request, additions, target = nil)
82
+ config, target = config_and_target(request, target)
83
+
84
+ if [:both, :enforced].include?(target)
85
+ if config.csp.opt_out?
86
+ config.csp = ContentSecurityPolicyConfig.new({})
87
+ end
88
+
89
+ config.csp.merge!(additions)
90
+ end
91
+
92
+ if [:both, :report_only].include?(target)
93
+ if config.csp_report_only.opt_out?
94
+ config.csp_report_only = ContentSecurityPolicyReportOnlyConfig.new({})
95
+ end
96
+
97
+ config.csp_report_only.merge!(additions)
57
98
  end
58
99
 
59
- config.dynamic_csp = config.current_csp.merge(additions)
60
100
  override_secure_headers_request_config(request, config)
61
101
  end
62
102
 
@@ -66,9 +106,17 @@ module SecureHeaders
66
106
  #
67
107
  # additions - a hash containing directives. e.g.
68
108
  # script_src: %w(another-host.com)
69
- def append_content_security_policy_directives(request, additions)
70
- config = config_for(request)
71
- config.dynamic_csp = CSP.combine_policies(config.current_csp, additions)
109
+ def append_content_security_policy_directives(request, additions, target = nil)
110
+ config, target = config_and_target(request, target)
111
+
112
+ if [:both, :enforced].include?(target) && !config.csp.opt_out?
113
+ config.csp.append(additions)
114
+ end
115
+
116
+ if [:both, :report_only].include?(target) && !config.csp_report_only.opt_out?
117
+ config.csp_report_only.append(additions)
118
+ end
119
+
72
120
  override_secure_headers_request_config(request, config)
73
121
  end
74
122
 
@@ -112,12 +160,28 @@ module SecureHeaders
112
160
  # returned is meant to be merged into the header value from `@app.call(env)`
113
161
  # in Rack middleware.
114
162
  def header_hash_for(request)
115
- config = config_for(request)
116
- unless ContentSecurityPolicy.idempotent_additions?(config.csp, config.current_csp)
117
- config.rebuild_csp_header_cache!(request.user_agent)
163
+ config = config_for(request, prevent_dup = true)
164
+ headers = config.cached_headers
165
+ user_agent = UserAgent.parse(request.user_agent)
166
+
167
+ if !config.csp.opt_out? && config.csp.modified?
168
+ headers = update_cached_csp(config.csp, headers, user_agent)
118
169
  end
119
170
 
120
- use_cached_headers(config.cached_headers, request)
171
+ if !config.csp_report_only.opt_out? && config.csp_report_only.modified?
172
+ headers = update_cached_csp(config.csp_report_only, headers, user_agent)
173
+ end
174
+
175
+ header_classes_for(request).each_with_object({}) do |klass, hash|
176
+ if header = headers[klass::CONFIG_KEY]
177
+ header_name, value = if [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig].include?(klass)
178
+ csp_header_for_ua(header, user_agent)
179
+ else
180
+ header
181
+ end
182
+ hash[header_name] = value
183
+ end
184
+ end
121
185
  end
122
186
 
123
187
  # Public: specify which named override will be used for this request.
@@ -138,7 +202,7 @@ module SecureHeaders
138
202
  #
139
203
  # Returns the nonce
140
204
  def content_security_policy_script_nonce(request)
141
- content_security_policy_nonce(request, CSP::SCRIPT_SRC)
205
+ content_security_policy_nonce(request, ContentSecurityPolicy::SCRIPT_SRC)
142
206
  end
143
207
 
144
208
  # Public: gets or creates a nonce for CSP.
@@ -147,7 +211,7 @@ module SecureHeaders
147
211
  #
148
212
  # Returns the nonce
149
213
  def content_security_policy_style_nonce(request)
150
- content_security_policy_nonce(request, CSP::STYLE_SRC)
214
+ content_security_policy_nonce(request, ContentSecurityPolicy::STYLE_SRC)
151
215
  end
152
216
 
153
217
  # Public: Retreives the config for a given header type:
@@ -155,11 +219,15 @@ module SecureHeaders
155
219
  # Checks to see if there is an override for this request, then
156
220
  # Checks to see if a named override is used for this request, then
157
221
  # Falls back to the global config
158
- def config_for(request)
222
+ def config_for(request, prevent_dup = false)
159
223
  config = request.env[SECURE_HEADERS_CONFIG] ||
160
224
  Configuration.get(Configuration::DEFAULT_CONFIG)
161
225
 
162
- if config.frozen?
226
+
227
+ # Global configs are frozen, per-request configs are not. When we're not
228
+ # making modifications to the config, prevent_dup ensures we don't dup
229
+ # the object unnecessarily. It's not necessarily frozen to begin with.
230
+ if config.frozen? && !prevent_dup
163
231
  config.dup
164
232
  else
165
233
  config
@@ -167,13 +235,38 @@ module SecureHeaders
167
235
  end
168
236
 
169
237
  private
238
+ TARGETS = [:both, :enforced, :report_only]
239
+ def raise_on_unknown_target(target)
240
+ unless TARGETS.include?(target)
241
+ raise "Unrecognized target: #{target}. Must be [:both, :enforced, :report_only]"
242
+ end
243
+ end
244
+
245
+ def config_and_target(request, target)
246
+ config = config_for(request)
247
+ target = guess_target(config) unless target
248
+ raise_on_unknown_target(target)
249
+ [config, target]
250
+ end
251
+
252
+ def guess_target(config)
253
+ if !config.csp.opt_out? && !config.csp_report_only.opt_out?
254
+ :both
255
+ elsif !config.csp.opt_out?
256
+ :enforced
257
+ elsif !config.csp_report_only.opt_out?
258
+ :report_only
259
+ else
260
+ :both
261
+ end
262
+ end
170
263
 
171
264
  # Private: gets or creates a nonce for CSP.
172
265
  #
173
266
  # Returns the nonce
174
267
  def content_security_policy_nonce(request, script_or_style)
175
268
  request.env[NONCE_KEY] ||= SecureRandom.base64(32).chomp
176
- nonce_key = script_or_style == CSP::SCRIPT_SRC ? :script_nonce : :style_nonce
269
+ nonce_key = script_or_style == ContentSecurityPolicy::SCRIPT_SRC ? :script_nonce : :style_nonce
177
270
  append_content_security_policy_directives(request, nonce_key => request.env[NONCE_KEY])
178
271
  request.env[NONCE_KEY]
179
272
  end
@@ -198,21 +291,12 @@ module SecureHeaders
198
291
  end
199
292
  end
200
293
 
201
- # Private: takes a precomputed hash of headers and returns the Headers
202
- # customized for the request.
203
- #
204
- # Returns a hash of header names / values valid for a given request.
205
- def use_cached_headers(headers, request)
206
- header_classes_for(request).each_with_object({}) do |klass, hash|
207
- if header = headers[klass::CONFIG_KEY]
208
- header_name, value = if klass == CSP
209
- csp_header_for_ua(header, request)
210
- else
211
- header
212
- end
213
- hash[header_name] = value
214
- end
215
- end
294
+ def update_cached_csp(config, headers, user_agent)
295
+ headers = Configuration.send(:deep_copy, headers)
296
+ headers[config.class::CONFIG_KEY] = {}
297
+ variation = ContentSecurityPolicy.ua_to_variation(user_agent)
298
+ headers[config.class::CONFIG_KEY][variation] = ContentSecurityPolicy.make_header(config, user_agent)
299
+ headers
216
300
  end
217
301
 
218
302
  # Private: chooses the applicable CSP header for the provided user agent.
@@ -220,26 +304,8 @@ module SecureHeaders
220
304
  # headers - a hash of header_config_key => [header_name, header_value]
221
305
  #
222
306
  # Returns a CSP [header, value] array
223
- def csp_header_for_ua(headers, request)
224
- headers[CSP.ua_to_variation(UserAgent.parse(request.user_agent))]
225
- end
226
-
227
- # Private: optionally build a header with a given configure
228
- #
229
- # klass - corresponding Class for a given header
230
- # config - A string, symbol, or hash config for the header
231
- # user_agent - A string representing the UA (only used for CSP feature sniffing)
232
- #
233
- # Returns a 2 element array [header_name, header_value] or nil if config
234
- # is OPT_OUT
235
- def make_header(klass, header_config, user_agent = nil)
236
- unless header_config == OPT_OUT
237
- if klass == CSP
238
- klass.make_header(header_config, user_agent)
239
- else
240
- klass.make_header(header_config)
241
- end
242
- end
307
+ def csp_header_for_ua(headers, user_agent)
308
+ headers[ContentSecurityPolicy.ua_to_variation(user_agent)]
243
309
  end
244
310
  end
245
311
 
@@ -85,7 +85,6 @@ module SecureHeaders
85
85
  ALL_HEADER_CLASSES.each do |klass|
86
86
  config.send("#{klass::CONFIG_KEY}=", OPT_OUT)
87
87
  end
88
- config.dynamic_csp = OPT_OUT
89
88
  end
90
89
 
91
90
  add_configuration(NOOP_CONFIGURATION, noop_config)
@@ -94,6 +93,7 @@ module SecureHeaders
94
93
  # Public: perform a basic deep dup. The shallow copy provided by dup/clone
95
94
  # can lead to modifying parent objects.
96
95
  def deep_copy(config)
96
+ return unless config
97
97
  config.each_with_object({}) do |(key, value), hash|
98
98
  hash[key] = if value.is_a?(Array)
99
99
  value.dup
@@ -114,13 +114,11 @@ module SecureHeaders
114
114
  end
115
115
  end
116
116
 
117
- attr_accessor :dynamic_csp
118
-
119
117
  attr_writer :hsts, :x_frame_options, :x_content_type_options,
120
118
  :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
121
119
  :referrer_policy
122
120
 
123
- attr_reader :cached_headers, :csp, :cookies, :hpkp, :hpkp_report_host
121
+ attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host
124
122
 
125
123
  HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml"
126
124
  if File.exists?(HASH_CONFIG_FILE)
@@ -132,7 +130,8 @@ module SecureHeaders
132
130
  def initialize(&block)
133
131
  self.hpkp = OPT_OUT
134
132
  self.referrer_policy = OPT_OUT
135
- self.csp = self.class.send(:deep_copy, CSP::DEFAULT_CONFIG)
133
+ self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT)
134
+ self.csp_report_only = OPT_OUT
136
135
  instance_eval &block if block_given?
137
136
  end
138
137
 
@@ -142,8 +141,8 @@ module SecureHeaders
142
141
  def dup
143
142
  copy = self.class.new
144
143
  copy.cookies = @cookies
145
- copy.csp = self.class.send(:deep_copy_if_hash, @csp)
146
- copy.dynamic_csp = self.class.send(:deep_copy_if_hash, @dynamic_csp)
144
+ copy.csp = @csp.dup if @csp
145
+ copy.csp_report_only = @csp_report_only.dup if @csp_report_only
147
146
  copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
148
147
  copy.x_content_type_options = @x_content_type_options
149
148
  copy.hsts = @hsts
@@ -159,9 +158,6 @@ module SecureHeaders
159
158
 
160
159
  def opt_out(header)
161
160
  send("#{header}=", OPT_OUT)
162
- if header == CSP::CONFIG_KEY
163
- dynamic_csp = OPT_OUT
164
- end
165
161
  self.cached_headers.delete(header)
166
162
  end
167
163
 
@@ -170,20 +166,6 @@ module SecureHeaders
170
166
  self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value)
171
167
  end
172
168
 
173
- # Public: generated cached headers for a specific user agent.
174
- def rebuild_csp_header_cache!(user_agent)
175
- self.cached_headers[CSP::CONFIG_KEY] = {}
176
- unless current_csp == OPT_OUT
177
- user_agent = UserAgent.parse(user_agent)
178
- variation = CSP.ua_to_variation(user_agent)
179
- self.cached_headers[CSP::CONFIG_KEY][variation] = CSP.make_header(current_csp, user_agent)
180
- end
181
- end
182
-
183
- def current_csp
184
- @dynamic_csp || @csp
185
- end
186
-
187
169
  # Public: validates all configurations values.
188
170
  #
189
171
  # Raises various configuration errors if any invalid config is detected.
@@ -192,6 +174,7 @@ module SecureHeaders
192
174
  def validate_config!
193
175
  StrictTransportSecurity.validate_config!(@hsts)
194
176
  ContentSecurityPolicy.validate_config!(@csp)
177
+ ContentSecurityPolicy.validate_config!(@csp_report_only)
195
178
  ReferrerPolicy.validate_config!(@referrer_policy)
196
179
  XFrameOptions.validate_config!(@x_frame_options)
197
180
  XContentTypeOptions.validate_config!(@x_content_type_options)
@@ -207,16 +190,50 @@ module SecureHeaders
207
190
  @cookies = (@cookies || {}).merge(secure: secure_cookies)
208
191
  end
209
192
 
210
- protected
211
-
212
193
  def csp=(new_csp)
213
- if self.dynamic_csp
214
- raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= instead."
194
+ if new_csp.respond_to?(:opt_out?)
195
+ @csp = new_csp.dup
196
+ else
197
+ if new_csp[:report_only]
198
+ # Deprecated configuration implies that CSPRO should be set, CSP should not - so opt out
199
+ Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp=` was supplied a config with report_only: true. Use #csp_report_only="
200
+ @csp = OPT_OUT
201
+ self.csp_report_only = new_csp
202
+ else
203
+ @csp = ContentSecurityPolicyConfig.new(new_csp)
204
+ end
205
+ end
206
+ end
207
+
208
+ # Configures the Content-Security-Policy-Report-Only header. `new_csp` cannot
209
+ # contain `report_only: false` or an error will be raised.
210
+ #
211
+ # NOTE: if csp has not been configured/has the default value when
212
+ # configuring csp_report_only, the code will assume you mean to only use
213
+ # report-only mode and you will be opted-out of enforce mode.
214
+ def csp_report_only=(new_csp)
215
+ @csp_report_only = begin
216
+ if new_csp.is_a?(ContentSecurityPolicyConfig)
217
+ new_csp.make_report_only
218
+ elsif new_csp.respond_to?(:opt_out?)
219
+ new_csp.dup
220
+ else
221
+ if new_csp[:report_only] == false # nil is a valid value on which we do not want to raise
222
+ raise ContentSecurityPolicyConfigError, "`#csp_report_only=` was supplied a config with report_only: false. Use #csp="
223
+ else
224
+ ContentSecurityPolicyReportOnlyConfig.new(new_csp)
225
+ end
226
+ end
215
227
  end
216
228
 
217
- @csp = self.class.send(:deep_copy_if_hash, new_csp)
229
+ if !@csp_report_only.opt_out? && @csp.to_h == ContentSecurityPolicyConfig::DEFAULT
230
+ 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="
231
+ @csp = OPT_OUT
232
+ end
218
233
  end
219
234
 
235
+ protected
236
+
220
237
  def cookies=(cookies)
221
238
  @cookies = cookies
222
239
  end
@@ -269,12 +286,16 @@ module SecureHeaders
269
286
  #
270
287
  # Returns nothing
271
288
  def generate_csp_headers(headers)
272
- unless @csp == OPT_OUT
273
- headers[CSP::CONFIG_KEY] = {}
274
- csp_config = self.current_csp
275
- CSP::VARIATIONS.each do |name, _|
276
- csp = CSP.make_header(csp_config, UserAgent.parse(name))
277
- headers[CSP::CONFIG_KEY][name] = csp.freeze
289
+ generate_csp_headers_for_config(headers, ContentSecurityPolicyConfig::CONFIG_KEY, self.csp)
290
+ generate_csp_headers_for_config(headers, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY, self.csp_report_only)
291
+ end
292
+
293
+ def generate_csp_headers_for_config(headers, header_key, csp_config)
294
+ unless csp_config.opt_out?
295
+ headers[header_key] = {}
296
+ ContentSecurityPolicy::VARIATIONS.each do |name, _|
297
+ csp = ContentSecurityPolicy.make_header(csp_config, UserAgent.parse(name))
298
+ headers[header_key][name] = csp.freeze
278
299
  end
279
300
  end
280
301
  end