secure_headers 3.7.2 → 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.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.rubocop.yml +3 -0
- data/.ruby-version +1 -1
- data/.travis.yml +8 -6
- data/CHANGELOG.md +2 -6
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +7 -4
- data/Guardfile +1 -0
- data/README.md +7 -19
- data/Rakefile +22 -18
- data/docs/cookies.md +16 -2
- data/docs/per_action_configuration.md +28 -0
- data/lib/secure_headers/configuration.rb +5 -12
- data/lib/secure_headers/hash_helper.rb +2 -1
- data/lib/secure_headers/headers/clear_site_data.rb +4 -3
- data/lib/secure_headers/headers/content_security_policy.rb +12 -15
- data/lib/secure_headers/headers/content_security_policy_config.rb +1 -1
- data/lib/secure_headers/headers/cookie.rb +21 -3
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +1 -1
- data/lib/secure_headers/headers/policy_management.rb +14 -8
- data/lib/secure_headers/headers/public_key_pins.rb +4 -3
- data/lib/secure_headers/headers/referrer_policy.rb +1 -0
- data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
- data/lib/secure_headers/headers/x_content_type_options.rb +1 -0
- data/lib/secure_headers/headers/x_download_options.rb +2 -1
- data/lib/secure_headers/headers/x_frame_options.rb +1 -0
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +2 -1
- data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
- data/lib/secure_headers/middleware.rb +10 -9
- data/lib/secure_headers/railtie.rb +7 -6
- data/lib/secure_headers/utils/cookies_config.rb +13 -12
- data/lib/secure_headers/view_helper.rb +2 -1
- data/lib/secure_headers.rb +2 -1
- data/lib/tasks/tasks.rake +2 -1
- data/secure_headers.gemspec +13 -3
- data/spec/lib/secure_headers/configuration_spec.rb +9 -8
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +2 -1
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +18 -18
- data/spec/lib/secure_headers/headers/cookie_spec.rb +38 -20
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +26 -11
- data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +4 -3
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +5 -4
- data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
- data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
- data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
- data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
- data/spec/lib/secure_headers/middleware_spec.rb +29 -21
- data/spec/lib/secure_headers/view_helpers_spec.rb +5 -4
- data/spec/lib/secure_headers_spec.rb +92 -120
- data/spec/spec_helper.rb +9 -22
- data/upgrading-to-4-0.md +55 -0
- metadata +13 -6
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "spec_helper"
|
|
2
3
|
|
|
3
4
|
module SecureHeaders
|
|
4
5
|
describe SecureHeaders do
|
|
@@ -30,16 +31,16 @@ module SecureHeaders
|
|
|
30
31
|
describe "#header_hash_for" do
|
|
31
32
|
it "allows you to opt out of individual headers via API" do
|
|
32
33
|
Configuration.default do |config|
|
|
33
|
-
config.csp = { default_src: %w('self')}
|
|
34
|
+
config.csp = { default_src: %w('self'), script_src: %w('self')}
|
|
34
35
|
config.csp_report_only = config.csp
|
|
35
36
|
end
|
|
36
37
|
SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY)
|
|
37
38
|
SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY)
|
|
38
39
|
SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY)
|
|
39
40
|
hash = SecureHeaders.header_hash_for(request)
|
|
40
|
-
expect(hash[
|
|
41
|
-
expect(hash[
|
|
42
|
-
expect(hash[
|
|
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
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
it "Carries options over when using overrides" do
|
|
@@ -54,15 +55,15 @@ module SecureHeaders
|
|
|
54
55
|
|
|
55
56
|
SecureHeaders.use_secure_headers_override(request, :api)
|
|
56
57
|
hash = SecureHeaders.header_hash_for(request)
|
|
57
|
-
expect(hash[
|
|
58
|
-
expect(hash[
|
|
59
|
-
expect(hash[
|
|
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
|
|
60
61
|
end
|
|
61
62
|
|
|
62
63
|
it "allows you to opt out entirely" do
|
|
63
64
|
# configure the disabled-by-default headers to ensure they also do not get set
|
|
64
65
|
Configuration.default do |config|
|
|
65
|
-
config.csp = { :
|
|
66
|
+
config.csp = { default_src: ["example.com"], script_src: %w('self') }
|
|
66
67
|
config.csp_report_only = config.csp
|
|
67
68
|
config.hpkp = {
|
|
68
69
|
report_only: false,
|
|
@@ -108,6 +109,7 @@ module SecureHeaders
|
|
|
108
109
|
Configuration.default do |config|
|
|
109
110
|
config.csp = {
|
|
110
111
|
default_src: %w('self'),
|
|
112
|
+
script_src: %w('self'),
|
|
111
113
|
child_src: %w('self')
|
|
112
114
|
}
|
|
113
115
|
end
|
|
@@ -144,10 +146,10 @@ module SecureHeaders
|
|
|
144
146
|
config.hpkp = {
|
|
145
147
|
max_age: 1_000_000,
|
|
146
148
|
include_subdomains: true,
|
|
147
|
-
report_uri:
|
|
149
|
+
report_uri: "//example.com/uri-directive",
|
|
148
150
|
pins: [
|
|
149
|
-
{ sha256:
|
|
150
|
-
{ sha256:
|
|
151
|
+
{ sha256: "abc" },
|
|
152
|
+
{ sha256: "123" }
|
|
151
153
|
]
|
|
152
154
|
}
|
|
153
155
|
end
|
|
@@ -173,42 +175,32 @@ module SecureHeaders
|
|
|
173
175
|
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
|
|
174
176
|
end
|
|
175
177
|
|
|
176
|
-
it "
|
|
178
|
+
it "child-src and frame-src must match" do
|
|
177
179
|
Configuration.default do |config|
|
|
178
180
|
config.csp = {
|
|
179
181
|
default_src: %w('self'),
|
|
180
|
-
frame_src: %w(frame_src.com)
|
|
182
|
+
frame_src: %w(frame_src.com),
|
|
183
|
+
script_src: %w('self')
|
|
181
184
|
}
|
|
182
185
|
end
|
|
183
186
|
|
|
184
187
|
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
|
|
188
|
-
|
|
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
|
|
196
188
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src child_src.com frame_src.com")
|
|
189
|
+
expect {
|
|
190
|
+
SecureHeaders.header_hash_for(chrome_request)
|
|
191
|
+
}.to raise_error(ArgumentError)
|
|
201
192
|
end
|
|
202
193
|
|
|
203
194
|
it "supports named appends" do
|
|
204
195
|
Configuration.default do |config|
|
|
205
196
|
config.csp = {
|
|
206
|
-
default_src: %w('self')
|
|
197
|
+
default_src: %w('self'),
|
|
198
|
+
script_src: %w('self')
|
|
207
199
|
}
|
|
208
200
|
end
|
|
209
201
|
|
|
210
202
|
Configuration.named_append(:moar_default_sources) do |request|
|
|
211
|
-
{ default_src: %w(https:)}
|
|
203
|
+
{ default_src: %w(https:), style_src: %w('self')}
|
|
212
204
|
end
|
|
213
205
|
|
|
214
206
|
Configuration.named_append(:how_about_a_script_src_too) do |request|
|
|
@@ -219,13 +211,14 @@ module SecureHeaders
|
|
|
219
211
|
SecureHeaders.use_content_security_policy_named_append(request, :how_about_a_script_src_too)
|
|
220
212
|
hash = SecureHeaders.header_hash_for(request)
|
|
221
213
|
|
|
222
|
-
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; script-src 'self'
|
|
214
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; script-src 'self' 'unsafe-inline'; style-src 'self'")
|
|
223
215
|
end
|
|
224
216
|
|
|
225
217
|
it "appends a nonce to a missing script-src value" do
|
|
226
218
|
Configuration.default do |config|
|
|
227
219
|
config.csp = {
|
|
228
|
-
default_src: %w('self')
|
|
220
|
+
default_src: %w('self'),
|
|
221
|
+
script_src: %w('self')
|
|
229
222
|
}
|
|
230
223
|
end
|
|
231
224
|
|
|
@@ -237,7 +230,8 @@ module SecureHeaders
|
|
|
237
230
|
it "appends a hash to a missing script-src value" do
|
|
238
231
|
Configuration.default do |config|
|
|
239
232
|
config.csp = {
|
|
240
|
-
default_src: %w('self')
|
|
233
|
+
default_src: %w('self'),
|
|
234
|
+
script_src: %w('self')
|
|
241
235
|
}
|
|
242
236
|
end
|
|
243
237
|
|
|
@@ -249,24 +243,26 @@ module SecureHeaders
|
|
|
249
243
|
it "overrides individual directives" do
|
|
250
244
|
Configuration.default do |config|
|
|
251
245
|
config.csp = {
|
|
252
|
-
default_src: %w('self')
|
|
246
|
+
default_src: %w('self'),
|
|
247
|
+
script_src: %w('self')
|
|
253
248
|
}
|
|
254
249
|
end
|
|
255
250
|
SecureHeaders.override_content_security_policy_directives(request, default_src: %w('none'))
|
|
256
251
|
hash = SecureHeaders.header_hash_for(request)
|
|
257
|
-
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'none'")
|
|
252
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'none'; script-src 'self'")
|
|
258
253
|
end
|
|
259
254
|
|
|
260
255
|
it "overrides non-existant directives" do
|
|
261
256
|
Configuration.default do |config|
|
|
262
257
|
config.csp = {
|
|
263
|
-
default_src: %w(https:)
|
|
258
|
+
default_src: %w(https:),
|
|
259
|
+
script_src: %w('self')
|
|
264
260
|
}
|
|
265
261
|
end
|
|
266
262
|
SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL])
|
|
267
263
|
hash = SecureHeaders.header_hash_for(request)
|
|
268
264
|
expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to be_nil
|
|
269
|
-
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; img-src data
|
|
265
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; img-src data:; script-src 'self'")
|
|
270
266
|
end
|
|
271
267
|
|
|
272
268
|
it "does not append a nonce when the browser does not support it" do
|
|
@@ -301,7 +297,7 @@ module SecureHeaders
|
|
|
301
297
|
SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
302
298
|
|
|
303
299
|
hash = SecureHeaders.header_hash_for(chrome_request)
|
|
304
|
-
expect(hash[
|
|
300
|
+
expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'")
|
|
305
301
|
end
|
|
306
302
|
|
|
307
303
|
it "uses a nonce for safari 10+" do
|
|
@@ -315,25 +311,18 @@ module SecureHeaders
|
|
|
315
311
|
safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari10]))
|
|
316
312
|
nonce = SecureHeaders.content_security_policy_script_nonce(safari_request)
|
|
317
313
|
hash = SecureHeaders.header_hash_for(safari_request)
|
|
318
|
-
expect(hash[
|
|
314
|
+
expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'")
|
|
319
315
|
end
|
|
320
316
|
|
|
321
|
-
it "
|
|
322
|
-
expect
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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'")
|
|
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)
|
|
337
326
|
end
|
|
338
327
|
|
|
339
328
|
it "Raises an error if csp_report_only is used with `report_only: false`" do
|
|
@@ -341,6 +330,7 @@ module SecureHeaders
|
|
|
341
330
|
Configuration.default do |config|
|
|
342
331
|
config.csp_report_only = {
|
|
343
332
|
default_src: %w('self'),
|
|
333
|
+
script_src: %w('self'),
|
|
344
334
|
report_only: false
|
|
345
335
|
}
|
|
346
336
|
end
|
|
@@ -351,7 +341,8 @@ module SecureHeaders
|
|
|
351
341
|
before(:each) do
|
|
352
342
|
Configuration.default do |config|
|
|
353
343
|
config.csp = {
|
|
354
|
-
default_src: %w('self')
|
|
344
|
+
default_src: %w('self'),
|
|
345
|
+
script_src: %w('self')
|
|
355
346
|
}
|
|
356
347
|
config.csp_report_only = config.csp
|
|
357
348
|
end
|
|
@@ -360,155 +351,136 @@ module SecureHeaders
|
|
|
360
351
|
it "sets identical values when the configs are the same" do
|
|
361
352
|
Configuration.default do |config|
|
|
362
353
|
config.csp = {
|
|
363
|
-
default_src: %w('self')
|
|
354
|
+
default_src: %w('self'),
|
|
355
|
+
script_src: %w('self')
|
|
364
356
|
}
|
|
365
357
|
config.csp_report_only = {
|
|
366
|
-
default_src: %w('self')
|
|
358
|
+
default_src: %w('self'),
|
|
359
|
+
script_src: %w('self')
|
|
367
360
|
}
|
|
368
361
|
end
|
|
369
362
|
|
|
370
363
|
hash = SecureHeaders.header_hash_for(request)
|
|
371
|
-
expect(hash[
|
|
372
|
-
expect(hash[
|
|
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'")
|
|
373
366
|
end
|
|
374
367
|
|
|
375
368
|
it "sets different headers when the configs are different" do
|
|
376
369
|
Configuration.default do |config|
|
|
377
370
|
config.csp = {
|
|
378
|
-
default_src: %w('self')
|
|
371
|
+
default_src: %w('self'),
|
|
372
|
+
script_src: %w('self')
|
|
379
373
|
}
|
|
380
|
-
config.csp_report_only = config.csp.merge({script_src: %w(
|
|
374
|
+
config.csp_report_only = config.csp.merge({script_src: %w(foo.com)})
|
|
381
375
|
end
|
|
382
376
|
|
|
383
377
|
hash = SecureHeaders.header_hash_for(request)
|
|
384
|
-
expect(hash[
|
|
385
|
-
expect(hash[
|
|
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")
|
|
386
380
|
end
|
|
387
381
|
|
|
388
382
|
it "allows you to opt-out of enforced CSP" do
|
|
389
383
|
Configuration.default do |config|
|
|
390
384
|
config.csp = SecureHeaders::OPT_OUT
|
|
391
385
|
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')
|
|
386
|
+
default_src: %w('self'),
|
|
387
|
+
script_src: %w('self')
|
|
419
388
|
}
|
|
420
|
-
config.csp = config.csp_report_only.merge({script_src: %w('unsafe-inline')})
|
|
421
389
|
end
|
|
422
390
|
|
|
423
391
|
hash = SecureHeaders.header_hash_for(request)
|
|
424
|
-
expect(hash[
|
|
425
|
-
expect(hash[
|
|
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'")
|
|
426
394
|
end
|
|
427
395
|
|
|
428
396
|
it "allows appending to the enforced policy" do
|
|
429
397
|
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced)
|
|
430
398
|
hash = SecureHeaders.header_hash_for(request)
|
|
431
|
-
expect(hash[
|
|
432
|
-
expect(hash[
|
|
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'")
|
|
433
401
|
end
|
|
434
402
|
|
|
435
403
|
it "allows appending to the report only policy" do
|
|
436
404
|
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only)
|
|
437
405
|
hash = SecureHeaders.header_hash_for(request)
|
|
438
|
-
expect(hash[
|
|
439
|
-
expect(hash[
|
|
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")
|
|
440
408
|
end
|
|
441
409
|
|
|
442
410
|
it "allows appending to both policies" do
|
|
443
411
|
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both)
|
|
444
412
|
hash = SecureHeaders.header_hash_for(request)
|
|
445
|
-
expect(hash[
|
|
446
|
-
expect(hash[
|
|
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")
|
|
447
415
|
end
|
|
448
416
|
|
|
449
417
|
it "allows overriding the enforced policy" do
|
|
450
418
|
SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced)
|
|
451
419
|
hash = SecureHeaders.header_hash_for(request)
|
|
452
|
-
expect(hash[
|
|
453
|
-
expect(hash[
|
|
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'")
|
|
454
422
|
end
|
|
455
423
|
|
|
456
424
|
it "allows overriding the report only policy" do
|
|
457
425
|
SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only)
|
|
458
426
|
hash = SecureHeaders.header_hash_for(request)
|
|
459
|
-
expect(hash[
|
|
460
|
-
expect(hash[
|
|
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")
|
|
461
429
|
end
|
|
462
430
|
|
|
463
431
|
it "allows overriding both policies" do
|
|
464
432
|
SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both)
|
|
465
433
|
hash = SecureHeaders.header_hash_for(request)
|
|
466
|
-
expect(hash[
|
|
467
|
-
expect(hash[
|
|
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")
|
|
468
436
|
end
|
|
469
437
|
|
|
470
438
|
context "when inferring which config to modify" do
|
|
471
439
|
it "updates the enforced header when configured" do
|
|
472
440
|
Configuration.default do |config|
|
|
473
441
|
config.csp = {
|
|
474
|
-
default_src: %w('self')
|
|
442
|
+
default_src: %w('self'),
|
|
443
|
+
script_src: %w('self')
|
|
475
444
|
}
|
|
476
445
|
end
|
|
477
446
|
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
|
|
478
447
|
|
|
479
448
|
hash = SecureHeaders.header_hash_for(request)
|
|
480
|
-
expect(hash[
|
|
481
|
-
expect(hash[
|
|
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
|
|
482
451
|
end
|
|
483
452
|
|
|
484
453
|
it "updates the report only header when configured" do
|
|
485
454
|
Configuration.default do |config|
|
|
486
455
|
config.csp = OPT_OUT
|
|
487
456
|
config.csp_report_only = {
|
|
488
|
-
default_src: %w('self')
|
|
457
|
+
default_src: %w('self'),
|
|
458
|
+
script_src: %w('self')
|
|
489
459
|
}
|
|
490
460
|
end
|
|
491
461
|
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
|
|
492
462
|
|
|
493
463
|
hash = SecureHeaders.header_hash_for(request)
|
|
494
|
-
expect(hash[
|
|
495
|
-
expect(hash[
|
|
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
|
|
496
466
|
end
|
|
497
467
|
|
|
498
468
|
it "updates both headers if both are configured" do
|
|
499
469
|
Configuration.default do |config|
|
|
500
470
|
config.csp = {
|
|
501
|
-
default_src: %w(enforced.com)
|
|
471
|
+
default_src: %w(enforced.com),
|
|
472
|
+
script_src: %w('self')
|
|
502
473
|
}
|
|
503
474
|
config.csp_report_only = {
|
|
504
|
-
default_src: %w(reportonly.com)
|
|
475
|
+
default_src: %w(reportonly.com),
|
|
476
|
+
script_src: %w('self')
|
|
505
477
|
}
|
|
506
478
|
end
|
|
507
479
|
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
|
|
508
480
|
|
|
509
481
|
hash = SecureHeaders.header_hash_for(request)
|
|
510
|
-
expect(hash[
|
|
511
|
-
expect(hash[
|
|
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")
|
|
512
484
|
end
|
|
513
485
|
|
|
514
486
|
end
|
|
@@ -520,7 +492,7 @@ module SecureHeaders
|
|
|
520
492
|
it "validates your hsts config upon configuration" do
|
|
521
493
|
expect do
|
|
522
494
|
Configuration.default do |config|
|
|
523
|
-
config.hsts =
|
|
495
|
+
config.hsts = "lol"
|
|
524
496
|
end
|
|
525
497
|
end.to raise_error(STSConfigError)
|
|
526
498
|
end
|
|
@@ -528,7 +500,7 @@ module SecureHeaders
|
|
|
528
500
|
it "validates your csp config upon configuration" do
|
|
529
501
|
expect do
|
|
530
502
|
Configuration.default do |config|
|
|
531
|
-
config.csp = { ContentSecurityPolicy::DEFAULT_SRC =>
|
|
503
|
+
config.csp = { ContentSecurityPolicy::DEFAULT_SRC => "123456" }
|
|
532
504
|
end
|
|
533
505
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
534
506
|
end
|
|
@@ -536,7 +508,7 @@ module SecureHeaders
|
|
|
536
508
|
it "raises errors for unknown directives" do
|
|
537
509
|
expect do
|
|
538
510
|
Configuration.default do |config|
|
|
539
|
-
config.csp = { made_up_directive:
|
|
511
|
+
config.csp = { made_up_directive: "123456" }
|
|
540
512
|
end
|
|
541
513
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
542
514
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
require
|
|
3
|
-
require
|
|
4
|
-
require
|
|
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__),
|
|
8
|
+
require File.join(File.dirname(__FILE__), "..", "lib", "secure_headers")
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
|
|
@@ -14,9 +13,9 @@ 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:
|
|
18
|
-
ie:
|
|
19
|
-
opera:
|
|
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",
|
|
@@ -50,15 +49,3 @@ end
|
|
|
50
49
|
def reset_config
|
|
51
50
|
SecureHeaders::Configuration.clear_configurations
|
|
52
51
|
end
|
|
53
|
-
|
|
54
|
-
def capture_warning
|
|
55
|
-
begin
|
|
56
|
-
old_stderr = $stderr
|
|
57
|
-
$stderr = StringIO.new
|
|
58
|
-
yield
|
|
59
|
-
result = $stderr.string
|
|
60
|
-
ensure
|
|
61
|
-
$stderr = old_stderr
|
|
62
|
-
end
|
|
63
|
-
result
|
|
64
|
-
end
|
data/upgrading-to-4-0.md
ADDED
|
@@ -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.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: secure_headers
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Neil Matatall
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2017-
|
|
11
|
+
date: 2017-09-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -30,14 +30,14 @@ dependencies:
|
|
|
30
30
|
requirements:
|
|
31
31
|
- - ">="
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version:
|
|
33
|
+
version: 0.15.0
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version:
|
|
40
|
+
version: 0.15.0
|
|
41
41
|
description: Manages application of security headers with many safe defaults.
|
|
42
42
|
email:
|
|
43
43
|
- neil.matatall@gmail.com
|
|
@@ -49,6 +49,7 @@ files:
|
|
|
49
49
|
- ".github/PULL_REQUEST_TEMPLATE.md"
|
|
50
50
|
- ".gitignore"
|
|
51
51
|
- ".rspec"
|
|
52
|
+
- ".rubocop.yml"
|
|
52
53
|
- ".ruby-gemset"
|
|
53
54
|
- ".ruby-version"
|
|
54
55
|
- ".travis.yml"
|
|
@@ -108,11 +109,17 @@ files:
|
|
|
108
109
|
- spec/lib/secure_headers_spec.rb
|
|
109
110
|
- spec/spec_helper.rb
|
|
110
111
|
- upgrading-to-3-0.md
|
|
112
|
+
- upgrading-to-4-0.md
|
|
111
113
|
homepage: https://github.com/twitter/secureheaders
|
|
112
114
|
licenses:
|
|
113
115
|
- Apache Public License 2.0
|
|
114
116
|
metadata: {}
|
|
115
|
-
post_install_message:
|
|
117
|
+
post_install_message: |2+
|
|
118
|
+
|
|
119
|
+
**********
|
|
120
|
+
:wave: secure_headers 4.0 introduces a lot of breaking changes (in the name of security!). It's highly likely you will need to update your secure_headers cookie configuration to avoid breaking things. See the upgrade guide for details: https://github.com/twitter/secureheaders/blob/master/upgrading-to-4-0.md
|
|
121
|
+
**********
|
|
122
|
+
|
|
116
123
|
rdoc_options: []
|
|
117
124
|
require_paths:
|
|
118
125
|
- lib
|
|
@@ -128,7 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
128
135
|
version: '0'
|
|
129
136
|
requirements: []
|
|
130
137
|
rubyforge_project:
|
|
131
|
-
rubygems_version: 2.
|
|
138
|
+
rubygems_version: 2.6.11
|
|
132
139
|
signing_key:
|
|
133
140
|
specification_version: 4
|
|
134
141
|
summary: Add easily configured security headers to responses including content-security-policy,
|