secure_headers 3.0.3 → 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 (68) 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/.gitignore +0 -9
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +3 -0
  7. data/.ruby-version +1 -1
  8. data/.travis.yml +17 -5
  9. data/CHANGELOG.md +287 -0
  10. data/CODE_OF_CONDUCT.md +46 -0
  11. data/CONTRIBUTING.md +41 -0
  12. data/Gemfile +13 -5
  13. data/Guardfile +1 -0
  14. data/LICENSE +4 -199
  15. data/README.md +85 -265
  16. data/Rakefile +22 -18
  17. data/docs/HPKP.md +17 -0
  18. data/docs/cookies.md +64 -0
  19. data/docs/hashes.md +64 -0
  20. data/docs/named_overrides_and_appends.md +107 -0
  21. data/docs/per_action_configuration.md +133 -0
  22. data/docs/sinatra.md +25 -0
  23. data/lib/secure_headers/configuration.rb +188 -59
  24. data/lib/secure_headers/hash_helper.rb +11 -0
  25. data/lib/secure_headers/headers/clear_site_data.rb +55 -0
  26. data/lib/secure_headers/headers/content_security_policy.rb +136 -325
  27. data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
  28. data/lib/secure_headers/headers/cookie.rb +144 -0
  29. data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
  30. data/lib/secure_headers/headers/policy_management.rb +410 -0
  31. data/lib/secure_headers/headers/public_key_pins.rb +5 -4
  32. data/lib/secure_headers/headers/referrer_policy.rb +37 -0
  33. data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
  34. data/lib/secure_headers/headers/x_content_type_options.rb +3 -2
  35. data/lib/secure_headers/headers/x_download_options.rb +3 -2
  36. data/lib/secure_headers/headers/x_frame_options.rb +2 -1
  37. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +3 -2
  38. data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
  39. data/lib/secure_headers/middleware.rb +44 -0
  40. data/lib/secure_headers/railtie.rb +11 -6
  41. data/lib/secure_headers/utils/cookies_config.rb +95 -0
  42. data/lib/secure_headers/view_helper.rb +76 -5
  43. data/lib/secure_headers.rb +159 -99
  44. data/lib/tasks/tasks.rake +83 -0
  45. data/secure_headers.gemspec +13 -3
  46. data/spec/lib/secure_headers/configuration_spec.rb +24 -9
  47. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +87 -0
  48. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +88 -189
  49. data/spec/lib/secure_headers/headers/cookie_spec.rb +182 -0
  50. data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
  51. data/spec/lib/secure_headers/headers/policy_management_spec.rb +228 -0
  52. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
  53. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +73 -0
  54. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +6 -5
  55. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
  56. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
  57. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
  58. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
  59. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
  60. data/spec/lib/secure_headers/middleware_spec.rb +96 -6
  61. data/spec/lib/secure_headers/view_helpers_spec.rb +138 -0
  62. data/spec/lib/secure_headers_spec.rb +407 -58
  63. data/spec/spec_helper.rb +19 -12
  64. data/upgrading-to-3-0.md +13 -9
  65. data/upgrading-to-4-0.md +55 -0
  66. metadata +45 -9
  67. data/lib/secure_headers/padrino.rb +0 -13
  68. data/travis.sh +0 -10
@@ -1,58 +1,93 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
2
3
 
3
4
  module SecureHeaders
4
5
  describe SecureHeaders do
5
- example_hpkp_config = {
6
- max_age: 1_000_000,
7
- include_subdomains: true,
8
- report_uri: '//example.com/uri-directive',
9
- pins: [
10
- { sha256: 'abc' },
11
- { sha256: '123' }
12
- ]
13
- }
14
-
15
- example_hpkp_config_value = %(max-age=1000000; pin-sha256="abc"; pin-sha256="123"; report-uri="//example.com/uri-directive"; includeSubDomains)
16
-
17
6
  before(:each) do
18
7
  reset_config
19
- @request = Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on")
20
8
  end
21
9
 
10
+ let(:request) { Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on") }
11
+
22
12
  it "raises a NotYetConfiguredError if default has not been set" do
23
13
  expect do
24
- SecureHeaders.header_hash_for(@request)
14
+ SecureHeaders.header_hash_for(request)
25
15
  end.to raise_error(Configuration::NotYetConfiguredError)
26
16
  end
27
17
 
28
18
  it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do
29
19
  expect do
30
- SecureHeaders.opt_out_of_header(@request, CSP::CONFIG_KEY)
20
+ SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY)
31
21
  end.to raise_error(Configuration::NotYetConfiguredError)
32
22
  end
33
23
 
34
- describe "#header_hash_for" do
35
- it "allows you to opt out of individual headers" do
24
+ it "raises and ArgumentError when referencing an override that has not been set" do
25
+ expect do
36
26
  Configuration.default
37
- SecureHeaders.opt_out_of_header(@request, CSP::CONFIG_KEY)
38
- hash = SecureHeaders.header_hash_for(@request)
39
- expect(hash['Content-Security-Policy-Report-Only']).to be_nil
40
- expect(hash['Content-Security-Policy']).to be_nil
27
+ SecureHeaders.use_secure_headers_override(request, :missing)
28
+ end.to raise_error(ArgumentError)
29
+ end
30
+
31
+ describe "#header_hash_for" do
32
+ it "allows you to opt out of individual headers via API" do
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)
39
+ SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY)
40
+ hash = SecureHeaders.header_hash_for(request)
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
44
+ end
45
+
46
+ it "Carries options over when using overrides" do
47
+ Configuration.default do |config|
48
+ config.x_download_options = OPT_OUT
49
+ config.x_permitted_cross_domain_policies = OPT_OUT
50
+ end
51
+
52
+ Configuration.override(:api) do |config|
53
+ config.x_frame_options = OPT_OUT
54
+ end
55
+
56
+ SecureHeaders.use_secure_headers_override(request, :api)
57
+ hash = SecureHeaders.header_hash_for(request)
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
41
61
  end
42
62
 
43
63
  it "allows you to opt out entirely" do
44
- Configuration.default
45
- SecureHeaders.opt_out_of_all_protection(@request)
46
- hash = SecureHeaders.header_hash_for(@request)
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
79
+ SecureHeaders.opt_out_of_all_protection(request)
80
+ hash = SecureHeaders.header_hash_for(request)
47
81
  ALL_HEADER_CLASSES.each do |klass|
48
82
  expect(hash[klass::CONFIG_KEY]).to be_nil
49
83
  end
84
+ expect(hash.count).to eq(0)
50
85
  end
51
86
 
52
87
  it "allows you to override X-Frame-Options settings" do
53
88
  Configuration.default
54
- SecureHeaders.override_x_frame_options(@request, XFrameOptions::DENY)
55
- hash = SecureHeaders.header_hash_for(@request)
89
+ SecureHeaders.override_x_frame_options(request, XFrameOptions::DENY)
90
+ hash = SecureHeaders.header_hash_for(request)
56
91
  expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::DENY)
57
92
  end
58
93
 
@@ -62,17 +97,38 @@ module SecureHeaders
62
97
  config.csp = OPT_OUT
63
98
  end
64
99
 
65
- SecureHeaders.override_x_frame_options(@request, XFrameOptions::SAMEORIGIN)
66
- SecureHeaders.override_content_security_policy_directives(@request, default_src: %w(https:), script_src: %w('self'))
100
+ SecureHeaders.override_x_frame_options(request, XFrameOptions::SAMEORIGIN)
101
+ SecureHeaders.override_content_security_policy_directives(request, default_src: %w(https:), script_src: %w('self'))
67
102
 
68
- hash = SecureHeaders.header_hash_for(@request)
69
- expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; script-src 'self'")
103
+ hash = SecureHeaders.header_hash_for(request)
104
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; script-src 'self'")
70
105
  expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN)
71
106
  end
72
107
 
108
+ it "produces a UA-specific CSP when overriding (and busting the cache)" do
109
+ Configuration.default do |config|
110
+ config.csp = {
111
+ default_src: %w('self'),
112
+ script_src: %w('self'),
113
+ child_src: %w('self')
114
+ }
115
+ end
116
+ firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox]))
117
+
118
+ # append an unsupported directive
119
+ SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(flash)})
120
+ # append a supported directive
121
+ SecureHeaders.override_content_security_policy_directives(firefox_request, {script_src: %w('self')})
122
+
123
+ hash = SecureHeaders.header_hash_for(firefox_request)
124
+
125
+ # child-src is translated to frame-src
126
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
127
+ end
128
+
73
129
  it "produces a hash of headers with default config" do
74
130
  Configuration.default
75
- hash = SecureHeaders.header_hash_for(@request)
131
+ hash = SecureHeaders.header_hash_for(request)
76
132
  expect_default_values(hash)
77
133
  end
78
134
 
@@ -87,13 +143,25 @@ module SecureHeaders
87
143
  it "does not set the HPKP header if request is over HTTP" do
88
144
  plaintext_request = Rack::Request.new({})
89
145
  Configuration.default do |config|
90
- config.hpkp = example_hpkp_config
146
+ config.hpkp = {
147
+ max_age: 1_000_000,
148
+ include_subdomains: true,
149
+ report_uri: "//example.com/uri-directive",
150
+ pins: [
151
+ { sha256: "abc" },
152
+ { sha256: "123" }
153
+ ]
154
+ }
91
155
  end
92
156
 
93
157
  expect(SecureHeaders.header_hash_for(plaintext_request)[PublicKeyPins::HEADER_NAME]).to be_nil
94
158
  end
95
159
 
96
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
+
97
165
  it "appends a value to csp directive" do
98
166
  Configuration.default do |config|
99
167
  config.csp = {
@@ -102,27 +170,99 @@ module SecureHeaders
102
170
  }
103
171
  end
104
172
 
105
- SecureHeaders.append_content_security_policy_directives(@request, script_src: %w(anothercdn.com))
106
- hash = SecureHeaders.header_hash_for(@request)
107
- expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
173
+ SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
174
+ hash = SecureHeaders.header_hash_for(request)
175
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
176
+ end
177
+
178
+ it "child-src and frame-src must match" do
179
+ Configuration.default do |config|
180
+ config.csp = {
181
+ default_src: %w('self'),
182
+ frame_src: %w(frame_src.com),
183
+ script_src: %w('self')
184
+ }
185
+ end
186
+
187
+ SecureHeaders.append_content_security_policy_directives(chrome_request, child_src: %w(child_src.com))
188
+
189
+ expect {
190
+ SecureHeaders.header_hash_for(chrome_request)
191
+ }.to raise_error(ArgumentError)
192
+ end
193
+
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
201
+
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
209
+
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'")
215
+ end
216
+
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
224
+
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
229
+
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
237
+
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/)
108
241
  end
109
242
 
110
243
  it "overrides individual directives" do
111
244
  Configuration.default do |config|
112
245
  config.csp = {
113
- default_src: %w('self')
246
+ default_src: %w('self'),
247
+ script_src: %w('self')
114
248
  }
115
249
  end
116
- SecureHeaders.override_content_security_policy_directives(@request, default_src: %w('none'))
117
- hash = SecureHeaders.header_hash_for(@request)
118
- expect(hash[CSP::HEADER_NAME]).to eq("default-src 'none'")
250
+ SecureHeaders.override_content_security_policy_directives(request, default_src: %w('none'))
251
+ hash = SecureHeaders.header_hash_for(request)
252
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'none'; script-src 'self'")
119
253
  end
120
254
 
121
255
  it "overrides non-existant directives" do
122
- Configuration.default
123
- SecureHeaders.override_content_security_policy_directives(@request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL])
124
- hash = SecureHeaders.header_hash_for(@request)
125
- expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; img-src data:")
256
+ Configuration.default do |config|
257
+ config.csp = {
258
+ default_src: %w(https:),
259
+ script_src: %w('self')
260
+ }
261
+ end
262
+ SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL])
263
+ hash = SecureHeaders.header_hash_for(request)
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'")
126
266
  end
127
267
 
128
268
  it "does not append a nonce when the browser does not support it" do
@@ -134,10 +274,10 @@ module SecureHeaders
134
274
  }
135
275
  end
136
276
 
137
- request = Rack::Request.new(@request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5]))
138
- nonce = SecureHeaders.content_security_policy_script_nonce(request)
139
- hash = SecureHeaders.header_hash_for(request)
140
- expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
277
+ safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5]))
278
+ SecureHeaders.content_security_policy_script_nonce(safari_request)
279
+ hash = SecureHeaders.header_hash_for(safari_request)
280
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
141
281
  end
142
282
 
143
283
  it "appends a nonce to the script-src when used" do
@@ -149,16 +289,201 @@ module SecureHeaders
149
289
  }
150
290
  end
151
291
 
152
- request = Rack::Request.new(@request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
153
- nonce = SecureHeaders.content_security_policy_script_nonce(request)
292
+ nonce = SecureHeaders.content_security_policy_script_nonce(chrome_request)
154
293
 
155
294
  # simulate the nonce being used multiple times in a request:
156
- SecureHeaders.content_security_policy_script_nonce(request)
157
- SecureHeaders.content_security_policy_script_nonce(request)
158
- SecureHeaders.content_security_policy_script_nonce(request)
295
+ SecureHeaders.content_security_policy_script_nonce(chrome_request)
296
+ SecureHeaders.content_security_policy_script_nonce(chrome_request)
297
+ SecureHeaders.content_security_policy_script_nonce(chrome_request)
159
298
 
160
- hash = SecureHeaders.header_hash_for(request)
161
- expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'")
299
+ hash = SecureHeaders.header_hash_for(chrome_request)
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
162
487
  end
163
488
  end
164
489
  end
@@ -167,7 +492,7 @@ module SecureHeaders
167
492
  it "validates your hsts config upon configuration" do
168
493
  expect do
169
494
  Configuration.default do |config|
170
- config.hsts = 'lol'
495
+ config.hsts = "lol"
171
496
  end
172
497
  end.to raise_error(STSConfigError)
173
498
  end
@@ -175,7 +500,7 @@ module SecureHeaders
175
500
  it "validates your csp config upon configuration" do
176
501
  expect do
177
502
  Configuration.default do |config|
178
- config.csp = { CSP::DEFAULT_SRC => '123456' }
503
+ config.csp = { ContentSecurityPolicy::DEFAULT_SRC => "123456" }
179
504
  end
180
505
  end.to raise_error(ContentSecurityPolicyConfigError)
181
506
  end
@@ -183,7 +508,7 @@ module SecureHeaders
183
508
  it "raises errors for unknown directives" do
184
509
  expect do
185
510
  Configuration.default do |config|
186
- config.csp = { made_up_directive: '123456' }
511
+ config.csp = { made_up_directive: "123456" }
187
512
  end
188
513
  end.to raise_error(ContentSecurityPolicyConfigError)
189
514
  end
@@ -204,6 +529,14 @@ module SecureHeaders
204
529
  end.to raise_error(XContentTypeOptionsConfigError)
205
530
  end
206
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
+
207
540
  it "validates your x_xss config upon configuration" do
208
541
  expect do
209
542
  Configuration.default do |config|
@@ -228,6 +561,14 @@ module SecureHeaders
228
561
  end.to raise_error(XPCDPConfigError)
229
562
  end
230
563
 
564
+ it "validates your referrer_policy config upon configuration" do
565
+ expect do
566
+ Configuration.default do |config|
567
+ config.referrer_policy = "lol"
568
+ end
569
+ end.to raise_error(ReferrerPolicyConfigError)
570
+ end
571
+
231
572
  it "validates your hpkp config upon configuration" do
232
573
  expect do
233
574
  Configuration.default do |config|
@@ -235,6 +576,14 @@ module SecureHeaders
235
576
  end
236
577
  end.to raise_error(PublicKeyPinsConfigError)
237
578
  end
579
+
580
+ it "validates your cookies config upon configuration" do
581
+ expect do
582
+ Configuration.default do |config|
583
+ config.cookies = { secure: "lol" }
584
+ end
585
+ end.to raise_error(CookiesConfigError)
586
+ end
238
587
  end
239
588
  end
240
589
  end
data/spec/spec_helper.rb CHANGED
@@ -1,32 +1,39 @@
1
- require 'rubygems'
2
- require 'rspec'
3
- require 'rack'
4
- require 'pry-nav'
1
+ # frozen_string_literal: true
2
+ require "rubygems"
3
+ require "rspec"
4
+ require "rack"
5
+ require "coveralls"
6
+ Coveralls.wear!
7
+
8
+ require File.join(File.dirname(__FILE__), "..", "lib", "secure_headers")
5
9
 
6
- require File.join(File.dirname(__FILE__), '..', 'lib', 'secure_headers')
7
10
 
8
- ENV["RAILS_ENV"] = "test"
9
11
 
10
12
  USER_AGENTS = {
11
- firefox: 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1',
12
- 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',
13
- ie: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)',
14
- opera: 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00',
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",
14
+ firefox: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1",
15
+ firefox46: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:46.0) Gecko/20100101 Firefox/46.0",
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",
15
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",
16
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",
17
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",
18
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",
19
- 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"
20
25
  }
21
26
 
22
27
  def expect_default_values(hash)
23
- 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'")
24
29
  expect(hash[SecureHeaders::XFrameOptions::HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::DEFAULT_VALUE)
25
30
  expect(hash[SecureHeaders::XDownloadOptions::HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::DEFAULT_VALUE)
26
31
  expect(hash[SecureHeaders::StrictTransportSecurity::HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::DEFAULT_VALUE)
27
32
  expect(hash[SecureHeaders::XXssProtection::HEADER_NAME]).to eq(SecureHeaders::XXssProtection::DEFAULT_VALUE)
28
33
  expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE)
29
34
  expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE)
35
+ expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to be_nil
36
+ expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil
30
37
  end
31
38
 
32
39
  module SecureHeaders