secure_headers 3.2.0 → 3.7.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 (50) hide show
  1. checksums.yaml +4 -4
  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 +140 -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 +74 -334
  13. data/docs/HPKP.md +17 -0
  14. data/docs/cookies.md +50 -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 +89 -32
  22. data/lib/secure_headers/headers/content_security_policy_config.rb +161 -0
  23. data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
  24. data/lib/secure_headers/headers/policy_management.rb +84 -49
  25. data/lib/secure_headers/headers/public_key_pins.rb +1 -1
  26. data/lib/secure_headers/headers/referrer_policy.rb +36 -0
  27. data/lib/secure_headers/headers/strict_transport_security.rb +1 -1
  28. data/lib/secure_headers/headers/x_content_type_options.rb +1 -1
  29. data/lib/secure_headers/headers/x_download_options.rb +1 -1
  30. data/lib/secure_headers/headers/x_frame_options.rb +1 -1
  31. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +1 -1
  32. data/lib/secure_headers/headers/x_xss_protection.rb +1 -1
  33. data/lib/secure_headers/middleware.rb +6 -0
  34. data/lib/secure_headers/railtie.rb +1 -1
  35. data/lib/secure_headers/view_helper.rb +13 -7
  36. data/lib/secure_headers.rb +138 -55
  37. data/lib/tasks/tasks.rake +5 -4
  38. data/secure_headers.gemspec +2 -2
  39. data/spec/lib/secure_headers/configuration_spec.rb +5 -5
  40. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +86 -0
  41. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +69 -12
  42. data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
  43. data/spec/lib/secure_headers/headers/policy_management_spec.rb +28 -36
  44. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +72 -0
  45. data/spec/lib/secure_headers/middleware_spec.rb +31 -1
  46. data/spec/lib/secure_headers/view_helpers_spec.rb +19 -7
  47. data/spec/lib/secure_headers_spec.rb +344 -44
  48. data/spec/spec_helper.rb +8 -3
  49. data/upgrading-to-3-0.md +13 -10
  50. metadata +24 -4
@@ -16,14 +16,25 @@ module SecureHeaders
16
16
 
17
17
  it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do
18
18
  expect do
19
- SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY)
19
+ SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY)
20
20
  end.to raise_error(Configuration::NotYetConfiguredError)
21
21
  end
22
22
 
23
+ it "raises and ArgumentError when referencing an override that has not been set" do
24
+ expect do
25
+ Configuration.default
26
+ SecureHeaders.use_secure_headers_override(request, :missing)
27
+ end.to raise_error(ArgumentError)
28
+ end
29
+
23
30
  describe "#header_hash_for" do
24
31
  it "allows you to opt out of individual headers via API" do
25
- Configuration.default
26
- SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY)
32
+ Configuration.default do |config|
33
+ config.csp = { default_src: %w('self')}
34
+ config.csp_report_only = config.csp
35
+ end
36
+ SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY)
37
+ SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY)
27
38
  SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY)
28
39
  hash = SecureHeaders.header_hash_for(request)
29
40
  expect(hash['Content-Security-Policy-Report-Only']).to be_nil
@@ -49,7 +60,21 @@ module SecureHeaders
49
60
  end
50
61
 
51
62
  it "allows you to opt out entirely" do
52
- Configuration.default
63
+ # configure the disabled-by-default headers to ensure they also do not get set
64
+ Configuration.default do |config|
65
+ config.csp = { :default_src => ["example.com"] }
66
+ config.csp_report_only = config.csp
67
+ config.hpkp = {
68
+ report_only: false,
69
+ max_age: 10000000,
70
+ include_subdomains: true,
71
+ report_uri: "https://report-uri.io/example-hpkp",
72
+ pins: [
73
+ {sha256: "abc"},
74
+ {sha256: "123"}
75
+ ]
76
+ }
77
+ end
53
78
  SecureHeaders.opt_out_of_all_protection(request)
54
79
  hash = SecureHeaders.header_hash_for(request)
55
80
  ALL_HEADER_CLASSES.each do |klass|
@@ -75,27 +100,28 @@ module SecureHeaders
75
100
  SecureHeaders.override_content_security_policy_directives(request, default_src: %w(https:), script_src: %w('self'))
76
101
 
77
102
  hash = SecureHeaders.header_hash_for(request)
78
- expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; script-src 'self'")
103
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; script-src 'self'")
79
104
  expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN)
80
105
  end
81
106
 
82
107
  it "produces a UA-specific CSP when overriding (and busting the cache)" do
83
- config = Configuration.default do |config|
108
+ Configuration.default do |config|
84
109
  config.csp = {
85
110
  default_src: %w('self'),
86
- child_src: %w('self'), #unsupported by firefox
87
- frame_src: %w('self')
111
+ child_src: %w('self')
88
112
  }
89
113
  end
90
114
  firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox]))
91
115
 
92
116
  # append an unsupported directive
93
- SecureHeaders.override_content_security_policy_directives(firefox_request, plugin_types: %w(flash))
117
+ SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(flash)})
94
118
  # append a supported directive
95
- SecureHeaders.override_content_security_policy_directives(firefox_request, script_src: %w('self'))
119
+ SecureHeaders.override_content_security_policy_directives(firefox_request, {script_src: %w('self')})
96
120
 
97
121
  hash = SecureHeaders.header_hash_for(firefox_request)
98
- expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
122
+
123
+ # child-src is translated to frame-src
124
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
99
125
  end
100
126
 
101
127
  it "produces a hash of headers with default config" do
@@ -130,6 +156,10 @@ module SecureHeaders
130
156
  end
131
157
 
132
158
  context "content security policy" do
159
+ let(:chrome_request) {
160
+ Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
161
+ }
162
+
133
163
  it "appends a value to csp directive" do
134
164
  Configuration.default do |config|
135
165
  config.csp = {
@@ -140,48 +170,80 @@ module SecureHeaders
140
170
 
141
171
  SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
142
172
  hash = SecureHeaders.header_hash_for(request)
143
- expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
173
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
144
174
  end
145
175
 
146
- it "dups global configuration just once when overriding n times and only calls idempotent_additions? once" do
176
+ it "appends child-src to frame-src" do
147
177
  Configuration.default do |config|
148
178
  config.csp = {
149
- default_src: %w('self')
179
+ default_src: %w('self'),
180
+ frame_src: %w(frame_src.com)
150
181
  }
151
182
  end
152
183
 
153
- expect(CSP).to receive(:idempotent_additions?).once
184
+ SecureHeaders.append_content_security_policy_directives(chrome_request, child_src: %w(child_src.com))
185
+ hash = SecureHeaders.header_hash_for(chrome_request)
186
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; child-src frame_src.com child_src.com")
187
+ end
154
188
 
155
- # before an override occurs, the env is empty
156
- expect(request.env[SECURE_HEADERS_CONFIG]).to be_nil
189
+ it "appends frame-src to child-src" do
190
+ Configuration.default do |config|
191
+ config.csp = {
192
+ default_src: %w('self'),
193
+ child_src: %w(child_src.com)
194
+ }
195
+ end
157
196
 
158
- SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
159
- new_config = SecureHeaders.config_for(request)
160
- expect(new_config).to_not be(Configuration.get)
197
+ safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari6]))
198
+ SecureHeaders.append_content_security_policy_directives(safari_request, frame_src: %w(frame_src.com))
199
+ hash = SecureHeaders.header_hash_for(safari_request)
200
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src child_src.com frame_src.com")
201
+ end
161
202
 
162
- SecureHeaders.override_content_security_policy_directives(request, script_src: %w(yet.anothercdn.com))
163
- current_config = SecureHeaders.config_for(request)
164
- expect(current_config).to be(new_config)
203
+ it "supports named appends" do
204
+ Configuration.default do |config|
205
+ config.csp = {
206
+ default_src: %w('self')
207
+ }
208
+ end
209
+
210
+ Configuration.named_append(:moar_default_sources) do |request|
211
+ { default_src: %w(https:)}
212
+ end
165
213
 
166
- SecureHeaders.header_hash_for(request)
214
+ Configuration.named_append(:how_about_a_script_src_too) do |request|
215
+ { script_src: %w('unsafe-inline')}
216
+ end
217
+
218
+ SecureHeaders.use_content_security_policy_named_append(request, :moar_default_sources)
219
+ SecureHeaders.use_content_security_policy_named_append(request, :how_about_a_script_src_too)
220
+ hash = SecureHeaders.header_hash_for(request)
221
+
222
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; script-src 'self' https: 'unsafe-inline'")
167
223
  end
168
224
 
169
- it "doesn't allow you to muck with csp configs when a dynamic policy is in use" do
170
- default_config = Configuration.default
171
- expect { default_config.csp = {} }.to raise_error(NoMethodError)
225
+ it "appends a nonce to a missing script-src value" do
226
+ Configuration.default do |config|
227
+ config.csp = {
228
+ default_src: %w('self')
229
+ }
230
+ end
172
231
 
173
- # config is frozen
174
- expect { default_config.send(:csp=, {}) }.to raise_error(RuntimeError)
232
+ SecureHeaders.content_security_policy_script_nonce(request) # should add the value to the header
233
+ hash = SecureHeaders.header_hash_for(chrome_request)
234
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/\Adefault-src 'self'; script-src 'self' 'nonce-.*'\z/)
235
+ end
175
236
 
176
- SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
177
- new_config = SecureHeaders.config_for(request)
178
- expect { new_config.send(:csp=, {}) }.to raise_error(Configuration::IllegalPolicyModificationError)
237
+ it "appends a hash to a missing script-src value" do
238
+ Configuration.default do |config|
239
+ config.csp = {
240
+ default_src: %w('self')
241
+ }
242
+ end
179
243
 
180
- expect do
181
- new_config.instance_eval do
182
- new_config.csp = {}
183
- end
184
- end.to raise_error(Configuration::IllegalPolicyModificationError)
244
+ SecureHeaders.append_content_security_policy_directives(request, script_src: %w('sha256-abc123'))
245
+ hash = SecureHeaders.header_hash_for(chrome_request)
246
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/\Adefault-src 'self'; script-src 'self' 'sha256-abc123'\z/)
185
247
  end
186
248
 
187
249
  it "overrides individual directives" do
@@ -192,14 +254,19 @@ module SecureHeaders
192
254
  end
193
255
  SecureHeaders.override_content_security_policy_directives(request, default_src: %w('none'))
194
256
  hash = SecureHeaders.header_hash_for(request)
195
- expect(hash[CSP::HEADER_NAME]).to eq("default-src 'none'")
257
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'none'")
196
258
  end
197
259
 
198
260
  it "overrides non-existant directives" do
199
- Configuration.default
261
+ Configuration.default do |config|
262
+ config.csp = {
263
+ default_src: %w(https:)
264
+ }
265
+ end
200
266
  SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL])
201
267
  hash = SecureHeaders.header_hash_for(request)
202
- expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; img-src data:")
268
+ expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to be_nil
269
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; img-src data:")
203
270
  end
204
271
 
205
272
  it "does not append a nonce when the browser does not support it" do
@@ -212,9 +279,9 @@ module SecureHeaders
212
279
  end
213
280
 
214
281
  safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5]))
215
- nonce = SecureHeaders.content_security_policy_script_nonce(safari_request)
282
+ SecureHeaders.content_security_policy_script_nonce(safari_request)
216
283
  hash = SecureHeaders.header_hash_for(safari_request)
217
- expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
284
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
218
285
  end
219
286
 
220
287
  it "appends a nonce to the script-src when used" do
@@ -226,7 +293,6 @@ module SecureHeaders
226
293
  }
227
294
  end
228
295
 
229
- chrome_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
230
296
  nonce = SecureHeaders.content_security_policy_script_nonce(chrome_request)
231
297
 
232
298
  # simulate the nonce being used multiple times in a request:
@@ -237,6 +303,216 @@ module SecureHeaders
237
303
  hash = SecureHeaders.header_hash_for(chrome_request)
238
304
  expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'")
239
305
  end
306
+
307
+ it "uses a nonce for safari 10+" do
308
+ Configuration.default do |config|
309
+ config.csp = {
310
+ default_src: %w('self'),
311
+ script_src: %w(mycdn.com)
312
+ }
313
+ end
314
+
315
+ safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari10]))
316
+ nonce = SecureHeaders.content_security_policy_script_nonce(safari_request)
317
+ hash = SecureHeaders.header_hash_for(safari_request)
318
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'")
319
+ end
320
+
321
+ it "supports the deprecated `report_only: true` format" do
322
+ expect(Kernel).to receive(:warn).once
323
+
324
+ Configuration.default do |config|
325
+ config.csp = {
326
+ default_src: %w('self'),
327
+ report_only: true
328
+ }
329
+ end
330
+
331
+ expect(Configuration.get.csp).to eq(OPT_OUT)
332
+ expect(Configuration.get.csp_report_only).to be_a(ContentSecurityPolicyReportOnlyConfig)
333
+
334
+ hash = SecureHeaders.header_hash_for(request)
335
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to be_nil
336
+ expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to eq("default-src 'self'")
337
+ end
338
+
339
+ it "Raises an error if csp_report_only is used with `report_only: false`" do
340
+ expect do
341
+ Configuration.default do |config|
342
+ config.csp_report_only = {
343
+ default_src: %w('self'),
344
+ report_only: false
345
+ }
346
+ end
347
+ end.to raise_error(ContentSecurityPolicyConfigError)
348
+ end
349
+
350
+ context "setting two headers" do
351
+ before(:each) do
352
+ Configuration.default do |config|
353
+ config.csp = {
354
+ default_src: %w('self')
355
+ }
356
+ config.csp_report_only = config.csp
357
+ end
358
+ end
359
+
360
+ it "sets identical values when the configs are the same" do
361
+ Configuration.default do |config|
362
+ config.csp = {
363
+ default_src: %w('self')
364
+ }
365
+ config.csp_report_only = {
366
+ default_src: %w('self')
367
+ }
368
+ end
369
+
370
+ hash = SecureHeaders.header_hash_for(request)
371
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'")
372
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
373
+ end
374
+
375
+ it "sets different headers when the configs are different" do
376
+ Configuration.default do |config|
377
+ config.csp = {
378
+ default_src: %w('self')
379
+ }
380
+ config.csp_report_only = config.csp.merge({script_src: %w('self')})
381
+ end
382
+
383
+ hash = SecureHeaders.header_hash_for(request)
384
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'")
385
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self'")
386
+ end
387
+
388
+ it "allows you to opt-out of enforced CSP" do
389
+ Configuration.default do |config|
390
+ config.csp = SecureHeaders::OPT_OUT
391
+ config.csp_report_only = {
392
+ default_src: %w('self')
393
+ }
394
+ end
395
+
396
+ hash = SecureHeaders.header_hash_for(request)
397
+ expect(hash['Content-Security-Policy']).to be_nil
398
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
399
+ end
400
+
401
+ it "opts-out of enforced CSP when only csp_report_only is set" do
402
+ expect(Kernel).to receive(:warn).once
403
+ Configuration.default do |config|
404
+ config.csp_report_only = {
405
+ default_src: %w('self')
406
+ }
407
+ end
408
+
409
+ hash = SecureHeaders.header_hash_for(request)
410
+ expect(hash['Content-Security-Policy']).to be_nil
411
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
412
+ end
413
+
414
+ it "allows you to set csp_report_only before csp" do
415
+ expect(Kernel).to receive(:warn).once
416
+ Configuration.default do |config|
417
+ config.csp_report_only = {
418
+ default_src: %w('self')
419
+ }
420
+ config.csp = config.csp_report_only.merge({script_src: %w('unsafe-inline')})
421
+ end
422
+
423
+ hash = SecureHeaders.header_hash_for(request)
424
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' 'unsafe-inline'")
425
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
426
+ end
427
+
428
+ it "allows appending to the enforced policy" do
429
+ SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced)
430
+ hash = SecureHeaders.header_hash_for(request)
431
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com")
432
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
433
+ end
434
+
435
+ it "allows appending to the report only policy" do
436
+ SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only)
437
+ hash = SecureHeaders.header_hash_for(request)
438
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'")
439
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com")
440
+ end
441
+
442
+ it "allows appending to both policies" do
443
+ SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both)
444
+ hash = SecureHeaders.header_hash_for(request)
445
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com")
446
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com")
447
+ end
448
+
449
+ it "allows overriding the enforced policy" do
450
+ SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced)
451
+ hash = SecureHeaders.header_hash_for(request)
452
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src anothercdn.com")
453
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
454
+ end
455
+
456
+ it "allows overriding the report only policy" do
457
+ SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only)
458
+ hash = SecureHeaders.header_hash_for(request)
459
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'")
460
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src anothercdn.com")
461
+ end
462
+
463
+ it "allows overriding both policies" do
464
+ SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both)
465
+ hash = SecureHeaders.header_hash_for(request)
466
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src anothercdn.com")
467
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src anothercdn.com")
468
+ end
469
+
470
+ context "when inferring which config to modify" do
471
+ it "updates the enforced header when configured" do
472
+ Configuration.default do |config|
473
+ config.csp = {
474
+ default_src: %w('self')
475
+ }
476
+ end
477
+ SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
478
+
479
+ hash = SecureHeaders.header_hash_for(request)
480
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com")
481
+ expect(hash['Content-Security-Policy-Report-Only']).to be_nil
482
+ end
483
+
484
+ it "updates the report only header when configured" do
485
+ Configuration.default do |config|
486
+ config.csp = OPT_OUT
487
+ config.csp_report_only = {
488
+ default_src: %w('self')
489
+ }
490
+ end
491
+ SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
492
+
493
+ hash = SecureHeaders.header_hash_for(request)
494
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com")
495
+ expect(hash['Content-Security-Policy']).to be_nil
496
+ end
497
+
498
+ it "updates both headers if both are configured" do
499
+ Configuration.default do |config|
500
+ config.csp = {
501
+ default_src: %w(enforced.com)
502
+ }
503
+ config.csp_report_only = {
504
+ default_src: %w(reportonly.com)
505
+ }
506
+ end
507
+ SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
508
+
509
+ hash = SecureHeaders.header_hash_for(request)
510
+ expect(hash['Content-Security-Policy']).to eq("default-src enforced.com; script-src enforced.com anothercdn.com")
511
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src reportonly.com; script-src reportonly.com anothercdn.com")
512
+ end
513
+
514
+ end
515
+ end
240
516
  end
241
517
  end
242
518
 
@@ -252,7 +528,7 @@ module SecureHeaders
252
528
  it "validates your csp config upon configuration" do
253
529
  expect do
254
530
  Configuration.default do |config|
255
- config.csp = { CSP::DEFAULT_SRC => '123456' }
531
+ config.csp = { ContentSecurityPolicy::DEFAULT_SRC => '123456' }
256
532
  end
257
533
  end.to raise_error(ContentSecurityPolicyConfigError)
258
534
  end
@@ -281,6 +557,14 @@ module SecureHeaders
281
557
  end.to raise_error(XContentTypeOptionsConfigError)
282
558
  end
283
559
 
560
+ it "validates your clear site data config upon configuration" do
561
+ expect do
562
+ Configuration.default do |config|
563
+ config.clear_site_data = 1
564
+ end
565
+ end.to raise_error(ClearSiteDataConfigError)
566
+ end
567
+
284
568
  it "validates your x_xss config upon configuration" do
285
569
  expect do
286
570
  Configuration.default do |config|
@@ -305,6 +589,14 @@ module SecureHeaders
305
589
  end.to raise_error(XPCDPConfigError)
306
590
  end
307
591
 
592
+ it "validates your referrer_policy config upon configuration" do
593
+ expect do
594
+ Configuration.default do |config|
595
+ config.referrer_policy = "lol"
596
+ end
597
+ end.to raise_error(ReferrerPolicyConfigError)
598
+ end
599
+
308
600
  it "validates your hpkp config upon configuration" do
309
601
  expect do
310
602
  Configuration.default do |config|
@@ -312,6 +604,14 @@ module SecureHeaders
312
604
  end
313
605
  end.to raise_error(PublicKeyPinsConfigError)
314
606
  end
607
+
608
+ it "validates your cookies config upon configuration" do
609
+ expect do
610
+ Configuration.default do |config|
611
+ config.cookies = { secure: "lol" }
612
+ end
613
+ end.to raise_error(CookiesConfigError)
614
+ end
315
615
  end
316
616
  end
317
617
  end
data/spec/spec_helper.rb CHANGED
@@ -11,7 +11,9 @@ require File.join(File.dirname(__FILE__), '..', 'lib', 'secure_headers')
11
11
 
12
12
 
13
13
  USER_AGENTS = {
14
- firefox: 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1',
14
+ edge: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246",
15
+ firefox: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1",
16
+ firefox46: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:46.0) Gecko/20100101 Firefox/46.0",
15
17
  chrome: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.56 Safari/536.5',
16
18
  ie: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)',
17
19
  opera: 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00',
@@ -19,17 +21,20 @@ USER_AGENTS = {
19
21
  ios6: "Mozilla/5.0 (iPhone; CPU iPhone OS 614 like Mac OS X) AppleWebKit/536.26 (KHTML like Gecko) Version/6.0 Mobile/10B350 Safari/8536.25",
20
22
  safari5: "Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko ) Version/5.1 Mobile/9B176 Safari/7534.48.3",
21
23
  safari5_1: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10",
22
- safari6: "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/536.30.1 (KHTML like Gecko) Version/6.0.5 Safari/536.30.1"
24
+ safari6: "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/536.30.1 (KHTML like Gecko) Version/6.0.5 Safari/536.30.1",
25
+ safari10: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/602.2.11 (KHTML, like Gecko) Version/10.0.1 Safari/602.2.11"
23
26
  }
24
27
 
25
28
  def expect_default_values(hash)
26
- expect(hash[SecureHeaders::CSP::HEADER_NAME]).to eq(SecureHeaders::CSP::DEFAULT_VALUE)
29
+ expect(hash[SecureHeaders::ContentSecurityPolicyConfig::HEADER_NAME]).to eq("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'")
27
30
  expect(hash[SecureHeaders::XFrameOptions::HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::DEFAULT_VALUE)
28
31
  expect(hash[SecureHeaders::XDownloadOptions::HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::DEFAULT_VALUE)
29
32
  expect(hash[SecureHeaders::StrictTransportSecurity::HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::DEFAULT_VALUE)
30
33
  expect(hash[SecureHeaders::XXssProtection::HEADER_NAME]).to eq(SecureHeaders::XXssProtection::DEFAULT_VALUE)
31
34
  expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE)
32
35
  expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE)
36
+ expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to be_nil
37
+ expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil
33
38
  end
34
39
 
35
40
  module SecureHeaders
data/upgrading-to-3-0.md CHANGED
@@ -3,16 +3,19 @@
3
3
  Changes
4
4
  ==
5
5
 
6
- | What | < = 2.x | >= 3.0 |
7
- |----------------------------------|----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
8
- | Global configuration | `SecureHeaders::Configuration.configure` block | `SecureHeaders::Configuration.default` block |
9
- | All headers besides HPKP and CSP | Accept hashes as config values | Must be strings (validated during configuration) |
10
- | CSP directive values | Accepted space delimited strings OR arrays of strings | Must be arrays of strings |
11
- | CSP Nonce values in views | `@content_security_policy_nonce` | `content_security_policy_script_nonce` or `content_security_policy_style_nonce`
12
- | `self`/`none` source expressions | could be `self` / `none` / `'self'` / `'none'` | Must be `'self'` or `'none'` |
13
- | `inline` / `eval` source expressions | could be `inline`, `eval`, `'unsafe-inline'`, or `'unsafe-eval'` | Must be `'unsafe-eval'` or `'unsafe-inline'` |
14
- | Per-action configuration | override [`def secure_header_options_for(header, options)`](https://github.com/twitter/secureheaders/commit/bb9ebc6c12a677aad29af8e0f08ffd1def56efec#diff-04c6e90faac2675aa89e2176d2eec7d8R111) | Use [named overrides](https://github.com/twitter/secureheaders#named-overrides) or [per-action helpers](https://github.com/twitter/secureheaders#per-action-configuration) |
15
- | CSP/HPKP use `report_only` config that defaults to false | `enforce: false` | `report_only: false` |
6
+ | What | < = 2.x | >= 3.0 |
7
+ | ---------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
8
+ | Global configuration | `SecureHeaders::Configuration.configure` block | `SecureHeaders::Configuration.default` block |
9
+ | All headers besides HPKP and CSP | Accept hashes as config values | Must be strings (validated during configuration) |
10
+ | CSP directive values | Accepted space delimited strings OR arrays of strings | Must be arrays of strings |
11
+ | CSP Nonce values in views | `@content_security_policy_nonce` | `content_security_policy_nonce(:script)` or `content_security_policy_nonce(:style)` |
12
+ | nonce is no longer a source expression | `config.csp = "'self' 'nonce'"` | Remove `'nonce'` from source expression and use [nonce helpers](https://github.com/twitter/secureheaders#nonce). |
13
+ | `self`/`none` source expressions | Could be `self` / `none` / `'self'` / `'none'` | Must be `'self'` or `'none'` |
14
+ | `inline` / `eval` source expressions | Could be `inline`, `eval`, `'unsafe-inline'`, or `'unsafe-eval'` | Must be `'unsafe-eval'` or `'unsafe-inline'` |
15
+ | Per-action configuration | Override [`def secure_header_options_for(header, options)`](https://github.com/twitter/secureheaders/commit/bb9ebc6c12a677aad29af8e0f08ffd1def56efec#diff-04c6e90faac2675aa89e2176d2eec7d8R111) | Use [named overrides](https://github.com/twitter/secureheaders#named-overrides) or [per-action helpers](https://github.com/twitter/secureheaders#per-action-configuration) |
16
+ | CSP/HPKP use `report_only` config that defaults to false | `enforce: false` | `report_only: false` |
17
+ | Schemes in source expressions | Schemes were not stripped | Schemes are stripped by default to discourage mixed content. Setting `preserve_schemes: true` will revert to previous behavior |
18
+ | Opting out of default configuration | `skip_before_filter :set_x_download_options_header` or `config.x_download_options = false` | Within default block: `config.x_download_options = SecureHeaders::OPT_OUT` |
16
19
 
17
20
  Migrating to 3.x from <= 2.x
18
21
  ==