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
@@ -1,4 +1,5 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
2
3
 
3
4
  module SecureHeaders
4
5
  describe SecureHeaders do
@@ -16,7 +17,7 @@ module SecureHeaders
16
17
 
17
18
  it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do
18
19
  expect do
19
- SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY)
20
+ SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY)
20
21
  end.to raise_error(Configuration::NotYetConfiguredError)
21
22
  end
22
23
 
@@ -29,13 +30,17 @@ module SecureHeaders
29
30
 
30
31
  describe "#header_hash_for" do
31
32
  it "allows you to opt out of individual headers via API" do
32
- Configuration.default
33
- SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY)
33
+ Configuration.default do |config|
34
+ config.csp = { default_src: %w('self'), script_src: %w('self')}
35
+ config.csp_report_only = config.csp
36
+ end
37
+ SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY)
38
+ SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY)
34
39
  SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY)
35
40
  hash = SecureHeaders.header_hash_for(request)
36
- expect(hash['Content-Security-Policy-Report-Only']).to be_nil
37
- expect(hash['Content-Security-Policy']).to be_nil
38
- expect(hash['X-Content-Type-Options']).to be_nil
41
+ expect(hash["Content-Security-Policy-Report-Only"]).to be_nil
42
+ expect(hash["Content-Security-Policy"]).to be_nil
43
+ expect(hash["X-Content-Type-Options"]).to be_nil
39
44
  end
40
45
 
41
46
  it "Carries options over when using overrides" do
@@ -50,13 +55,27 @@ module SecureHeaders
50
55
 
51
56
  SecureHeaders.use_secure_headers_override(request, :api)
52
57
  hash = SecureHeaders.header_hash_for(request)
53
- expect(hash['X-Download-Options']).to be_nil
54
- expect(hash['X-Permitted-Cross-Domain-Policies']).to be_nil
55
- expect(hash['X-Frame-Options']).to be_nil
58
+ expect(hash["X-Download-Options"]).to be_nil
59
+ expect(hash["X-Permitted-Cross-Domain-Policies"]).to be_nil
60
+ expect(hash["X-Frame-Options"]).to be_nil
56
61
  end
57
62
 
58
63
  it "allows you to opt out entirely" do
59
- Configuration.default
64
+ # configure the disabled-by-default headers to ensure they also do not get set
65
+ Configuration.default do |config|
66
+ config.csp = { default_src: ["example.com"], script_src: %w('self') }
67
+ config.csp_report_only = config.csp
68
+ config.hpkp = {
69
+ report_only: false,
70
+ max_age: 10000000,
71
+ include_subdomains: true,
72
+ report_uri: "https://report-uri.io/example-hpkp",
73
+ pins: [
74
+ {sha256: "abc"},
75
+ {sha256: "123"}
76
+ ]
77
+ }
78
+ end
60
79
  SecureHeaders.opt_out_of_all_protection(request)
61
80
  hash = SecureHeaders.header_hash_for(request)
62
81
  ALL_HEADER_CLASSES.each do |klass|
@@ -82,28 +101,29 @@ module SecureHeaders
82
101
  SecureHeaders.override_content_security_policy_directives(request, default_src: %w(https:), script_src: %w('self'))
83
102
 
84
103
  hash = SecureHeaders.header_hash_for(request)
85
- expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; script-src 'self'")
104
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; script-src 'self'")
86
105
  expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN)
87
106
  end
88
107
 
89
108
  it "produces a UA-specific CSP when overriding (and busting the cache)" do
90
- config = Configuration.default do |config|
109
+ Configuration.default do |config|
91
110
  config.csp = {
92
111
  default_src: %w('self'),
112
+ script_src: %w('self'),
93
113
  child_src: %w('self')
94
114
  }
95
115
  end
96
116
  firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox]))
97
117
 
98
118
  # append an unsupported directive
99
- SecureHeaders.override_content_security_policy_directives(firefox_request, plugin_types: %w(flash))
119
+ SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(flash)})
100
120
  # append a supported directive
101
- SecureHeaders.override_content_security_policy_directives(firefox_request, script_src: %w('self'))
121
+ SecureHeaders.override_content_security_policy_directives(firefox_request, {script_src: %w('self')})
102
122
 
103
123
  hash = SecureHeaders.header_hash_for(firefox_request)
104
124
 
105
125
  # child-src is translated to frame-src
106
- expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
126
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
107
127
  end
108
128
 
109
129
  it "produces a hash of headers with default config" do
@@ -126,10 +146,10 @@ module SecureHeaders
126
146
  config.hpkp = {
127
147
  max_age: 1_000_000,
128
148
  include_subdomains: true,
129
- report_uri: '//example.com/uri-directive',
149
+ report_uri: "//example.com/uri-directive",
130
150
  pins: [
131
- { sha256: 'abc' },
132
- { sha256: '123' }
151
+ { sha256: "abc" },
152
+ { sha256: "123" }
133
153
  ]
134
154
  }
135
155
  end
@@ -138,6 +158,10 @@ module SecureHeaders
138
158
  end
139
159
 
140
160
  context "content security policy" do
161
+ let(:chrome_request) {
162
+ Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
163
+ }
164
+
141
165
  it "appends a value to csp directive" do
142
166
  Configuration.default do |config|
143
167
  config.csp = {
@@ -148,66 +172,97 @@ module SecureHeaders
148
172
 
149
173
  SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
150
174
  hash = SecureHeaders.header_hash_for(request)
151
- expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
175
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
152
176
  end
153
177
 
154
- it "dups global configuration just once when overriding n times and only calls idempotent_additions? once" do
178
+ it "child-src and frame-src must match" do
155
179
  Configuration.default do |config|
156
180
  config.csp = {
157
- default_src: %w('self')
181
+ default_src: %w('self'),
182
+ frame_src: %w(frame_src.com),
183
+ script_src: %w('self')
158
184
  }
159
185
  end
160
186
 
161
- expect(CSP).to receive(:idempotent_additions?).once
187
+ SecureHeaders.append_content_security_policy_directives(chrome_request, child_src: %w(child_src.com))
162
188
 
163
- # before an override occurs, the env is empty
164
- expect(request.env[SECURE_HEADERS_CONFIG]).to be_nil
189
+ expect {
190
+ SecureHeaders.header_hash_for(chrome_request)
191
+ }.to raise_error(ArgumentError)
192
+ end
165
193
 
166
- SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
167
- new_config = SecureHeaders.config_for(request)
168
- expect(new_config).to_not be(Configuration.get)
194
+ it "supports named appends" do
195
+ Configuration.default do |config|
196
+ config.csp = {
197
+ default_src: %w('self'),
198
+ script_src: %w('self')
199
+ }
200
+ end
169
201
 
170
- SecureHeaders.override_content_security_policy_directives(request, script_src: %w(yet.anothercdn.com))
171
- current_config = SecureHeaders.config_for(request)
172
- expect(current_config).to be(new_config)
202
+ Configuration.named_append(:moar_default_sources) do |request|
203
+ { default_src: %w(https:), style_src: %w('self')}
204
+ end
205
+
206
+ Configuration.named_append(:how_about_a_script_src_too) do |request|
207
+ { script_src: %w('unsafe-inline')}
208
+ end
173
209
 
174
- SecureHeaders.header_hash_for(request)
210
+ SecureHeaders.use_content_security_policy_named_append(request, :moar_default_sources)
211
+ SecureHeaders.use_content_security_policy_named_append(request, :how_about_a_script_src_too)
212
+ hash = SecureHeaders.header_hash_for(request)
213
+
214
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; script-src 'self' 'unsafe-inline'; style-src 'self'")
175
215
  end
176
216
 
177
- it "doesn't allow you to muck with csp configs when a dynamic policy is in use" do
178
- default_config = Configuration.default
179
- expect { default_config.csp = {} }.to raise_error(NoMethodError)
217
+ it "appends a nonce to a missing script-src value" do
218
+ Configuration.default do |config|
219
+ config.csp = {
220
+ default_src: %w('self'),
221
+ script_src: %w('self')
222
+ }
223
+ end
180
224
 
181
- # config is frozen
182
- expect { default_config.send(:csp=, {}) }.to raise_error(RuntimeError)
225
+ SecureHeaders.content_security_policy_script_nonce(request) # should add the value to the header
226
+ hash = SecureHeaders.header_hash_for(chrome_request)
227
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/\Adefault-src 'self'; script-src 'self' 'nonce-.*'\z/)
228
+ end
183
229
 
184
- SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
185
- new_config = SecureHeaders.config_for(request)
186
- expect { new_config.send(:csp=, {}) }.to raise_error(Configuration::IllegalPolicyModificationError)
230
+ it "appends a hash to a missing script-src value" do
231
+ Configuration.default do |config|
232
+ config.csp = {
233
+ default_src: %w('self'),
234
+ script_src: %w('self')
235
+ }
236
+ end
187
237
 
188
- expect do
189
- new_config.instance_eval do
190
- new_config.csp = {}
191
- end
192
- end.to raise_error(Configuration::IllegalPolicyModificationError)
238
+ SecureHeaders.append_content_security_policy_directives(request, script_src: %w('sha256-abc123'))
239
+ hash = SecureHeaders.header_hash_for(chrome_request)
240
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/\Adefault-src 'self'; script-src 'self' 'sha256-abc123'\z/)
193
241
  end
194
242
 
195
243
  it "overrides individual directives" do
196
244
  Configuration.default do |config|
197
245
  config.csp = {
198
- default_src: %w('self')
246
+ default_src: %w('self'),
247
+ script_src: %w('self')
199
248
  }
200
249
  end
201
250
  SecureHeaders.override_content_security_policy_directives(request, default_src: %w('none'))
202
251
  hash = SecureHeaders.header_hash_for(request)
203
- expect(hash[CSP::HEADER_NAME]).to eq("default-src 'none'")
252
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'none'; script-src 'self'")
204
253
  end
205
254
 
206
255
  it "overrides non-existant directives" do
207
- Configuration.default
256
+ Configuration.default do |config|
257
+ config.csp = {
258
+ default_src: %w(https:),
259
+ script_src: %w('self')
260
+ }
261
+ end
208
262
  SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL])
209
263
  hash = SecureHeaders.header_hash_for(request)
210
- expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; img-src data:")
264
+ expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to be_nil
265
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; img-src data:; script-src 'self'")
211
266
  end
212
267
 
213
268
  it "does not append a nonce when the browser does not support it" do
@@ -220,9 +275,9 @@ module SecureHeaders
220
275
  end
221
276
 
222
277
  safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5]))
223
- nonce = SecureHeaders.content_security_policy_script_nonce(safari_request)
278
+ SecureHeaders.content_security_policy_script_nonce(safari_request)
224
279
  hash = SecureHeaders.header_hash_for(safari_request)
225
- expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
280
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
226
281
  end
227
282
 
228
283
  it "appends a nonce to the script-src when used" do
@@ -234,7 +289,6 @@ module SecureHeaders
234
289
  }
235
290
  end
236
291
 
237
- chrome_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
238
292
  nonce = SecureHeaders.content_security_policy_script_nonce(chrome_request)
239
293
 
240
294
  # simulate the nonce being used multiple times in a request:
@@ -243,7 +297,193 @@ module SecureHeaders
243
297
  SecureHeaders.content_security_policy_script_nonce(chrome_request)
244
298
 
245
299
  hash = SecureHeaders.header_hash_for(chrome_request)
246
- expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'")
300
+ expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'")
301
+ end
302
+
303
+ it "uses a nonce for safari 10+" do
304
+ Configuration.default do |config|
305
+ config.csp = {
306
+ default_src: %w('self'),
307
+ script_src: %w(mycdn.com)
308
+ }
309
+ end
310
+
311
+ safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari10]))
312
+ nonce = SecureHeaders.content_security_policy_script_nonce(safari_request)
313
+ hash = SecureHeaders.header_hash_for(safari_request)
314
+ expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'")
315
+ end
316
+
317
+ it "does not support the deprecated `report_only: true` format" do
318
+ expect {
319
+ Configuration.default do |config|
320
+ config.csp = {
321
+ default_src: %w('self'),
322
+ report_only: true
323
+ }
324
+ end
325
+ }.to raise_error(ArgumentError)
326
+ end
327
+
328
+ it "Raises an error if csp_report_only is used with `report_only: false`" do
329
+ expect do
330
+ Configuration.default do |config|
331
+ config.csp_report_only = {
332
+ default_src: %w('self'),
333
+ script_src: %w('self'),
334
+ report_only: false
335
+ }
336
+ end
337
+ end.to raise_error(ContentSecurityPolicyConfigError)
338
+ end
339
+
340
+ context "setting two headers" do
341
+ before(:each) do
342
+ Configuration.default do |config|
343
+ config.csp = {
344
+ default_src: %w('self'),
345
+ script_src: %w('self')
346
+ }
347
+ config.csp_report_only = config.csp
348
+ end
349
+ end
350
+
351
+ it "sets identical values when the configs are the same" do
352
+ Configuration.default do |config|
353
+ config.csp = {
354
+ default_src: %w('self'),
355
+ script_src: %w('self')
356
+ }
357
+ config.csp_report_only = {
358
+ default_src: %w('self'),
359
+ script_src: %w('self')
360
+ }
361
+ end
362
+
363
+ hash = SecureHeaders.header_hash_for(request)
364
+ expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'")
365
+ expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'")
366
+ end
367
+
368
+ it "sets different headers when the configs are different" do
369
+ Configuration.default do |config|
370
+ config.csp = {
371
+ default_src: %w('self'),
372
+ script_src: %w('self')
373
+ }
374
+ config.csp_report_only = config.csp.merge({script_src: %w(foo.com)})
375
+ end
376
+
377
+ hash = SecureHeaders.header_hash_for(request)
378
+ expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'")
379
+ expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' foo.com")
380
+ end
381
+
382
+ it "allows you to opt-out of enforced CSP" do
383
+ Configuration.default do |config|
384
+ config.csp = SecureHeaders::OPT_OUT
385
+ config.csp_report_only = {
386
+ default_src: %w('self'),
387
+ script_src: %w('self')
388
+ }
389
+ end
390
+
391
+ hash = SecureHeaders.header_hash_for(request)
392
+ expect(hash["Content-Security-Policy"]).to be_nil
393
+ expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'")
394
+ end
395
+
396
+ it "allows appending to the enforced policy" do
397
+ SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced)
398
+ hash = SecureHeaders.header_hash_for(request)
399
+ expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com")
400
+ expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'")
401
+ end
402
+
403
+ it "allows appending to the report only policy" do
404
+ SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only)
405
+ hash = SecureHeaders.header_hash_for(request)
406
+ expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'")
407
+ expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com")
408
+ end
409
+
410
+ it "allows appending to both policies" do
411
+ SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both)
412
+ hash = SecureHeaders.header_hash_for(request)
413
+ expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com")
414
+ expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com")
415
+ end
416
+
417
+ it "allows overriding the enforced policy" do
418
+ SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced)
419
+ hash = SecureHeaders.header_hash_for(request)
420
+ expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src anothercdn.com")
421
+ expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'")
422
+ end
423
+
424
+ it "allows overriding the report only policy" do
425
+ SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only)
426
+ hash = SecureHeaders.header_hash_for(request)
427
+ expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'")
428
+ expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src anothercdn.com")
429
+ end
430
+
431
+ it "allows overriding both policies" do
432
+ SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both)
433
+ hash = SecureHeaders.header_hash_for(request)
434
+ expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src anothercdn.com")
435
+ expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src anothercdn.com")
436
+ end
437
+
438
+ context "when inferring which config to modify" do
439
+ it "updates the enforced header when configured" do
440
+ Configuration.default do |config|
441
+ config.csp = {
442
+ default_src: %w('self'),
443
+ script_src: %w('self')
444
+ }
445
+ end
446
+ SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
447
+
448
+ hash = SecureHeaders.header_hash_for(request)
449
+ expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com")
450
+ expect(hash["Content-Security-Policy-Report-Only"]).to be_nil
451
+ end
452
+
453
+ it "updates the report only header when configured" do
454
+ Configuration.default do |config|
455
+ config.csp = OPT_OUT
456
+ config.csp_report_only = {
457
+ default_src: %w('self'),
458
+ script_src: %w('self')
459
+ }
460
+ end
461
+ SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
462
+
463
+ hash = SecureHeaders.header_hash_for(request)
464
+ expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com")
465
+ expect(hash["Content-Security-Policy"]).to be_nil
466
+ end
467
+
468
+ it "updates both headers if both are configured" do
469
+ Configuration.default do |config|
470
+ config.csp = {
471
+ default_src: %w(enforced.com),
472
+ script_src: %w('self')
473
+ }
474
+ config.csp_report_only = {
475
+ default_src: %w(reportonly.com),
476
+ script_src: %w('self')
477
+ }
478
+ end
479
+ SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
480
+
481
+ hash = SecureHeaders.header_hash_for(request)
482
+ expect(hash["Content-Security-Policy"]).to eq("default-src enforced.com; script-src 'self' anothercdn.com")
483
+ expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src reportonly.com; script-src 'self' anothercdn.com")
484
+ end
485
+
486
+ end
247
487
  end
248
488
  end
249
489
  end
@@ -252,7 +492,7 @@ module SecureHeaders
252
492
  it "validates your hsts config upon configuration" do
253
493
  expect do
254
494
  Configuration.default do |config|
255
- config.hsts = 'lol'
495
+ config.hsts = "lol"
256
496
  end
257
497
  end.to raise_error(STSConfigError)
258
498
  end
@@ -260,7 +500,7 @@ module SecureHeaders
260
500
  it "validates your csp config upon configuration" do
261
501
  expect do
262
502
  Configuration.default do |config|
263
- config.csp = { CSP::DEFAULT_SRC => '123456' }
503
+ config.csp = { ContentSecurityPolicy::DEFAULT_SRC => "123456" }
264
504
  end
265
505
  end.to raise_error(ContentSecurityPolicyConfigError)
266
506
  end
@@ -268,7 +508,7 @@ module SecureHeaders
268
508
  it "raises errors for unknown directives" do
269
509
  expect do
270
510
  Configuration.default do |config|
271
- config.csp = { made_up_directive: '123456' }
511
+ config.csp = { made_up_directive: "123456" }
272
512
  end
273
513
  end.to raise_error(ContentSecurityPolicyConfigError)
274
514
  end
@@ -289,6 +529,14 @@ module SecureHeaders
289
529
  end.to raise_error(XContentTypeOptionsConfigError)
290
530
  end
291
531
 
532
+ it "validates your clear site data config upon configuration" do
533
+ expect do
534
+ Configuration.default do |config|
535
+ config.clear_site_data = 1
536
+ end
537
+ end.to raise_error(ClearSiteDataConfigError)
538
+ end
539
+
292
540
  it "validates your x_xss config upon configuration" do
293
541
  expect do
294
542
  Configuration.default do |config|
data/spec/spec_helper.rb CHANGED
@@ -1,12 +1,11 @@
1
- require 'rubygems'
2
- require 'rspec'
3
- require 'rack'
4
- require 'pry-nav'
5
-
6
- require 'coveralls'
1
+ # frozen_string_literal: true
2
+ require "rubygems"
3
+ require "rspec"
4
+ require "rack"
5
+ require "coveralls"
7
6
  Coveralls.wear!
8
7
 
9
- require File.join(File.dirname(__FILE__), '..', 'lib', 'secure_headers')
8
+ require File.join(File.dirname(__FILE__), "..", "lib", "secure_headers")
10
9
 
11
10
 
12
11
 
@@ -14,18 +13,19 @@ USER_AGENTS = {
14
13
  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
14
  firefox: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1",
16
15
  firefox46: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:46.0) Gecko/20100101 Firefox/46.0",
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',
18
- ie: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)',
19
- opera: 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00',
16
+ 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",
17
+ ie: "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)",
18
+ opera: "Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00",
20
19
  ios5: "Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3",
21
20
  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",
22
21
  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",
23
22
  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",
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"
23
+ 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
+ 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"
25
25
  }
26
26
 
27
27
  def expect_default_values(hash)
28
- expect(hash[SecureHeaders::CSP::HEADER_NAME]).to eq(SecureHeaders::CSP::DEFAULT_VALUE)
28
+ 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'")
29
29
  expect(hash[SecureHeaders::XFrameOptions::HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::DEFAULT_VALUE)
30
30
  expect(hash[SecureHeaders::XDownloadOptions::HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::DEFAULT_VALUE)
31
31
  expect(hash[SecureHeaders::StrictTransportSecurity::HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::DEFAULT_VALUE)
@@ -33,6 +33,7 @@ def expect_default_values(hash)
33
33
  expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE)
34
34
  expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE)
35
35
  expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to be_nil
36
+ expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil
36
37
  end
37
38
 
38
39
  module SecureHeaders
@@ -48,15 +49,3 @@ end
48
49
  def reset_config
49
50
  SecureHeaders::Configuration.clear_configurations
50
51
  end
51
-
52
- def capture_warning
53
- begin
54
- old_stderr = $stderr
55
- $stderr = StringIO.new
56
- yield
57
- result = $stderr.string
58
- ensure
59
- $stderr = old_stderr
60
- end
61
- result
62
- end
@@ -0,0 +1,55 @@
1
+ ### Breaking Changes
2
+
3
+ The most likely change to break your app is the new cookie defaults. This is the first place to check. If you're using the default CSP, your policy will change but your app should not break. This should not break brand new projects using secure_headers either.
4
+
5
+ ## All cookies default to secure/httponly/SameSite=Lax
6
+
7
+ By default, *all* cookies will be marked as `SameSite=lax`,`secure`, and `httponly`. To opt-out, supply `OPT_OUT` as the value for `SecureHeaders.cookies` or the individual configs. Setting these values to `false` will raise an error.
8
+
9
+ ```ruby
10
+ # specific opt outs
11
+ config.cookies = {
12
+ secure: OPT_OUT,
13
+ httponly: OPT_OUT,
14
+ samesite: OPT_OUT,
15
+ }
16
+
17
+ # nuclear option, just make things work again
18
+ config.cookies = OPT_OUT
19
+ ```
20
+
21
+ ## script_src must be set
22
+
23
+ Not setting a `script_src` value means your policy falls back to whatever `default_src` (also required) is set to. This can be very dangerous and indicates the policy is too loose.
24
+
25
+ However, sometimes you really don't need a `script-src` e.g. API responses (`default-src 'none'`) so you can set `script_src: SecureHeaders::OPT_OUT` to work around this.
26
+
27
+ ## Default Content Security Policy
28
+
29
+ The default CSP has changed to be more universal without sacrificing too much security.
30
+
31
+ * Flash/Java disabled by default
32
+ * `img-src` allows data: images and favicons (among others)
33
+ * `style-src` allows inline CSS by default (most find it impossible/impractical to remove inline content today)
34
+ * `form-action` (not governed by `default-src`, practically treated as `*`) is set to `'self'`
35
+
36
+ Previously, the default CSP was:
37
+
38
+ `Content-Security-Policy: default-src 'self'`
39
+
40
+ The new default policy is:
41
+
42
+ `default-src https:; form-action 'self'; img-src https: data: 'self'; object-src 'none'; script-src https:; style-src 'self' 'unsafe-inline' https:`
43
+
44
+ ## CSP configuration
45
+
46
+ * Setting `report_only: true` in a CSP config will raise an error. Instead, set `csp_report_only`.
47
+ * Setting `frame_src` and `child_src` when values don't match will raise an error. Just use `frame_src`.
48
+
49
+ ## config.secure_cookies removed
50
+
51
+ Use `config.cookies` instead.
52
+
53
+ ## Supported ruby versions
54
+
55
+ We've dropped support for ruby versions <= 2.2. Sorry.