secure_headers 3.4.1 → 3.5.0.pre

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.

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