secure_headers 3.6.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.
- checksums.yaml +4 -4
- data/.rspec +2 -0
- data/.rubocop.yml +3 -0
- data/.ruby-version +1 -1
- data/.travis.yml +8 -6
- data/CHANGELOG.md +25 -1
- data/CONTRIBUTING.md +3 -5
- data/Gemfile +7 -4
- data/Guardfile +1 -0
- data/README.md +15 -19
- data/Rakefile +22 -18
- data/docs/cookies.md +16 -2
- data/docs/hashes.md +1 -1
- data/docs/per_action_configuration.md +28 -0
- data/lib/secure_headers/configuration.rb +29 -16
- data/lib/secure_headers/hash_helper.rb +2 -1
- data/lib/secure_headers/headers/clear_site_data.rb +16 -12
- data/lib/secure_headers/headers/content_security_policy.rb +52 -20
- data/lib/secure_headers/headers/content_security_policy_config.rb +25 -0
- data/lib/secure_headers/headers/cookie.rb +21 -3
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
- data/lib/secure_headers/headers/policy_management.rb +119 -66
- data/lib/secure_headers/headers/public_key_pins.rb +5 -4
- 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 +4 -3
- data/lib/secure_headers.rb +6 -2
- 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 +11 -27
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +42 -26
- data/spec/lib/secure_headers/headers/cookie_spec.rb +38 -20
- data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +57 -10
- 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 -5
- data/spec/lib/secure_headers_spec.rb +106 -107
- data/spec/spec_helper.rb +10 -22
- data/upgrading-to-4-0.md +55 -0
- metadata +16 -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,
|
|
@@ -105,9 +106,10 @@ module SecureHeaders
|
|
|
105
106
|
end
|
|
106
107
|
|
|
107
108
|
it "produces a UA-specific CSP when overriding (and busting the cache)" do
|
|
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,15 +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
|
|
|
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
|
+
|
|
176
194
|
it "supports named appends" do
|
|
177
195
|
Configuration.default do |config|
|
|
178
196
|
config.csp = {
|
|
179
|
-
default_src: %w('self')
|
|
197
|
+
default_src: %w('self'),
|
|
198
|
+
script_src: %w('self')
|
|
180
199
|
}
|
|
181
200
|
end
|
|
182
201
|
|
|
183
202
|
Configuration.named_append(:moar_default_sources) do |request|
|
|
184
|
-
{ default_src: %w(https:)}
|
|
203
|
+
{ default_src: %w(https:), style_src: %w('self')}
|
|
185
204
|
end
|
|
186
205
|
|
|
187
206
|
Configuration.named_append(:how_about_a_script_src_too) do |request|
|
|
@@ -192,54 +211,58 @@ module SecureHeaders
|
|
|
192
211
|
SecureHeaders.use_content_security_policy_named_append(request, :how_about_a_script_src_too)
|
|
193
212
|
hash = SecureHeaders.header_hash_for(request)
|
|
194
213
|
|
|
195
|
-
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'")
|
|
196
215
|
end
|
|
197
216
|
|
|
198
217
|
it "appends a nonce to a missing script-src value" do
|
|
199
218
|
Configuration.default do |config|
|
|
200
219
|
config.csp = {
|
|
201
|
-
default_src: %w('self')
|
|
220
|
+
default_src: %w('self'),
|
|
221
|
+
script_src: %w('self')
|
|
202
222
|
}
|
|
203
223
|
end
|
|
204
224
|
|
|
205
225
|
SecureHeaders.content_security_policy_script_nonce(request) # should add the value to the header
|
|
206
226
|
hash = SecureHeaders.header_hash_for(chrome_request)
|
|
207
|
-
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match
|
|
227
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/\Adefault-src 'self'; script-src 'self' 'nonce-.*'\z/)
|
|
208
228
|
end
|
|
209
229
|
|
|
210
230
|
it "appends a hash to a missing script-src value" do
|
|
211
231
|
Configuration.default do |config|
|
|
212
232
|
config.csp = {
|
|
213
|
-
default_src: %w('self')
|
|
233
|
+
default_src: %w('self'),
|
|
234
|
+
script_src: %w('self')
|
|
214
235
|
}
|
|
215
236
|
end
|
|
216
237
|
|
|
217
238
|
SecureHeaders.append_content_security_policy_directives(request, script_src: %w('sha256-abc123'))
|
|
218
239
|
hash = SecureHeaders.header_hash_for(chrome_request)
|
|
219
|
-
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match
|
|
240
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/\Adefault-src 'self'; script-src 'self' 'sha256-abc123'\z/)
|
|
220
241
|
end
|
|
221
242
|
|
|
222
243
|
it "overrides individual directives" do
|
|
223
244
|
Configuration.default do |config|
|
|
224
245
|
config.csp = {
|
|
225
|
-
default_src: %w('self')
|
|
246
|
+
default_src: %w('self'),
|
|
247
|
+
script_src: %w('self')
|
|
226
248
|
}
|
|
227
249
|
end
|
|
228
250
|
SecureHeaders.override_content_security_policy_directives(request, default_src: %w('none'))
|
|
229
251
|
hash = SecureHeaders.header_hash_for(request)
|
|
230
|
-
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'none'")
|
|
252
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'none'; script-src 'self'")
|
|
231
253
|
end
|
|
232
254
|
|
|
233
255
|
it "overrides non-existant directives" do
|
|
234
256
|
Configuration.default do |config|
|
|
235
257
|
config.csp = {
|
|
236
|
-
default_src: %w(https:)
|
|
258
|
+
default_src: %w(https:),
|
|
259
|
+
script_src: %w('self')
|
|
237
260
|
}
|
|
238
261
|
end
|
|
239
262
|
SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL])
|
|
240
263
|
hash = SecureHeaders.header_hash_for(request)
|
|
241
264
|
expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to be_nil
|
|
242
|
-
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'")
|
|
243
266
|
end
|
|
244
267
|
|
|
245
268
|
it "does not append a nonce when the browser does not support it" do
|
|
@@ -252,7 +275,7 @@ module SecureHeaders
|
|
|
252
275
|
end
|
|
253
276
|
|
|
254
277
|
safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5]))
|
|
255
|
-
|
|
278
|
+
SecureHeaders.content_security_policy_script_nonce(safari_request)
|
|
256
279
|
hash = SecureHeaders.header_hash_for(safari_request)
|
|
257
280
|
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
|
|
258
281
|
end
|
|
@@ -274,7 +297,7 @@ module SecureHeaders
|
|
|
274
297
|
SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
275
298
|
|
|
276
299
|
hash = SecureHeaders.header_hash_for(chrome_request)
|
|
277
|
-
expect(hash[
|
|
300
|
+
expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'")
|
|
278
301
|
end
|
|
279
302
|
|
|
280
303
|
it "uses a nonce for safari 10+" do
|
|
@@ -288,25 +311,18 @@ module SecureHeaders
|
|
|
288
311
|
safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari10]))
|
|
289
312
|
nonce = SecureHeaders.content_security_policy_script_nonce(safari_request)
|
|
290
313
|
hash = SecureHeaders.header_hash_for(safari_request)
|
|
291
|
-
expect(hash[
|
|
314
|
+
expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'")
|
|
292
315
|
end
|
|
293
316
|
|
|
294
|
-
it "
|
|
295
|
-
expect
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
expect(Configuration.get.csp).to eq(OPT_OUT)
|
|
305
|
-
expect(Configuration.get.csp_report_only).to be_a(ContentSecurityPolicyReportOnlyConfig)
|
|
306
|
-
|
|
307
|
-
hash = SecureHeaders.header_hash_for(request)
|
|
308
|
-
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to be_nil
|
|
309
|
-
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)
|
|
310
326
|
end
|
|
311
327
|
|
|
312
328
|
it "Raises an error if csp_report_only is used with `report_only: false`" do
|
|
@@ -314,6 +330,7 @@ module SecureHeaders
|
|
|
314
330
|
Configuration.default do |config|
|
|
315
331
|
config.csp_report_only = {
|
|
316
332
|
default_src: %w('self'),
|
|
333
|
+
script_src: %w('self'),
|
|
317
334
|
report_only: false
|
|
318
335
|
}
|
|
319
336
|
end
|
|
@@ -324,7 +341,8 @@ module SecureHeaders
|
|
|
324
341
|
before(:each) do
|
|
325
342
|
Configuration.default do |config|
|
|
326
343
|
config.csp = {
|
|
327
|
-
default_src: %w('self')
|
|
344
|
+
default_src: %w('self'),
|
|
345
|
+
script_src: %w('self')
|
|
328
346
|
}
|
|
329
347
|
config.csp_report_only = config.csp
|
|
330
348
|
end
|
|
@@ -333,155 +351,136 @@ module SecureHeaders
|
|
|
333
351
|
it "sets identical values when the configs are the same" do
|
|
334
352
|
Configuration.default do |config|
|
|
335
353
|
config.csp = {
|
|
336
|
-
default_src: %w('self')
|
|
354
|
+
default_src: %w('self'),
|
|
355
|
+
script_src: %w('self')
|
|
337
356
|
}
|
|
338
357
|
config.csp_report_only = {
|
|
339
|
-
default_src: %w('self')
|
|
358
|
+
default_src: %w('self'),
|
|
359
|
+
script_src: %w('self')
|
|
340
360
|
}
|
|
341
361
|
end
|
|
342
362
|
|
|
343
363
|
hash = SecureHeaders.header_hash_for(request)
|
|
344
|
-
expect(hash[
|
|
345
|
-
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'")
|
|
346
366
|
end
|
|
347
367
|
|
|
348
368
|
it "sets different headers when the configs are different" do
|
|
349
369
|
Configuration.default do |config|
|
|
350
370
|
config.csp = {
|
|
351
|
-
default_src: %w('self')
|
|
371
|
+
default_src: %w('self'),
|
|
372
|
+
script_src: %w('self')
|
|
352
373
|
}
|
|
353
|
-
config.csp_report_only = config.csp.merge({script_src: %w(
|
|
374
|
+
config.csp_report_only = config.csp.merge({script_src: %w(foo.com)})
|
|
354
375
|
end
|
|
355
376
|
|
|
356
377
|
hash = SecureHeaders.header_hash_for(request)
|
|
357
|
-
expect(hash[
|
|
358
|
-
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")
|
|
359
380
|
end
|
|
360
381
|
|
|
361
382
|
it "allows you to opt-out of enforced CSP" do
|
|
362
383
|
Configuration.default do |config|
|
|
363
384
|
config.csp = SecureHeaders::OPT_OUT
|
|
364
385
|
config.csp_report_only = {
|
|
365
|
-
default_src: %w('self')
|
|
366
|
-
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
hash = SecureHeaders.header_hash_for(request)
|
|
370
|
-
expect(hash['Content-Security-Policy']).to be_nil
|
|
371
|
-
expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
it "opts-out of enforced CSP when only csp_report_only is set" do
|
|
375
|
-
expect(Kernel).to receive(:warn).once
|
|
376
|
-
Configuration.default do |config|
|
|
377
|
-
config.csp_report_only = {
|
|
378
|
-
default_src: %w('self')
|
|
379
|
-
}
|
|
380
|
-
end
|
|
381
|
-
|
|
382
|
-
hash = SecureHeaders.header_hash_for(request)
|
|
383
|
-
expect(hash['Content-Security-Policy']).to be_nil
|
|
384
|
-
expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
it "allows you to set csp_report_only before csp" do
|
|
388
|
-
expect(Kernel).to receive(:warn).once
|
|
389
|
-
Configuration.default do |config|
|
|
390
|
-
config.csp_report_only = {
|
|
391
|
-
default_src: %w('self')
|
|
386
|
+
default_src: %w('self'),
|
|
387
|
+
script_src: %w('self')
|
|
392
388
|
}
|
|
393
|
-
config.csp = config.csp_report_only.merge({script_src: %w('unsafe-inline')})
|
|
394
389
|
end
|
|
395
390
|
|
|
396
391
|
hash = SecureHeaders.header_hash_for(request)
|
|
397
|
-
expect(hash[
|
|
398
|
-
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'")
|
|
399
394
|
end
|
|
400
395
|
|
|
401
396
|
it "allows appending to the enforced policy" do
|
|
402
397
|
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced)
|
|
403
398
|
hash = SecureHeaders.header_hash_for(request)
|
|
404
|
-
expect(hash[
|
|
405
|
-
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'")
|
|
406
401
|
end
|
|
407
402
|
|
|
408
403
|
it "allows appending to the report only policy" do
|
|
409
404
|
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only)
|
|
410
405
|
hash = SecureHeaders.header_hash_for(request)
|
|
411
|
-
expect(hash[
|
|
412
|
-
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")
|
|
413
408
|
end
|
|
414
409
|
|
|
415
410
|
it "allows appending to both policies" do
|
|
416
411
|
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both)
|
|
417
412
|
hash = SecureHeaders.header_hash_for(request)
|
|
418
|
-
expect(hash[
|
|
419
|
-
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")
|
|
420
415
|
end
|
|
421
416
|
|
|
422
417
|
it "allows overriding the enforced policy" do
|
|
423
418
|
SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced)
|
|
424
419
|
hash = SecureHeaders.header_hash_for(request)
|
|
425
|
-
expect(hash[
|
|
426
|
-
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'")
|
|
427
422
|
end
|
|
428
423
|
|
|
429
424
|
it "allows overriding the report only policy" do
|
|
430
425
|
SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only)
|
|
431
426
|
hash = SecureHeaders.header_hash_for(request)
|
|
432
|
-
expect(hash[
|
|
433
|
-
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")
|
|
434
429
|
end
|
|
435
430
|
|
|
436
431
|
it "allows overriding both policies" do
|
|
437
432
|
SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both)
|
|
438
433
|
hash = SecureHeaders.header_hash_for(request)
|
|
439
|
-
expect(hash[
|
|
440
|
-
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")
|
|
441
436
|
end
|
|
442
437
|
|
|
443
438
|
context "when inferring which config to modify" do
|
|
444
439
|
it "updates the enforced header when configured" do
|
|
445
440
|
Configuration.default do |config|
|
|
446
441
|
config.csp = {
|
|
447
|
-
default_src: %w('self')
|
|
442
|
+
default_src: %w('self'),
|
|
443
|
+
script_src: %w('self')
|
|
448
444
|
}
|
|
449
445
|
end
|
|
450
446
|
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
|
|
451
447
|
|
|
452
448
|
hash = SecureHeaders.header_hash_for(request)
|
|
453
|
-
expect(hash[
|
|
454
|
-
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
|
|
455
451
|
end
|
|
456
452
|
|
|
457
453
|
it "updates the report only header when configured" do
|
|
458
454
|
Configuration.default do |config|
|
|
459
455
|
config.csp = OPT_OUT
|
|
460
456
|
config.csp_report_only = {
|
|
461
|
-
default_src: %w('self')
|
|
457
|
+
default_src: %w('self'),
|
|
458
|
+
script_src: %w('self')
|
|
462
459
|
}
|
|
463
460
|
end
|
|
464
461
|
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
|
|
465
462
|
|
|
466
463
|
hash = SecureHeaders.header_hash_for(request)
|
|
467
|
-
expect(hash[
|
|
468
|
-
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
|
|
469
466
|
end
|
|
470
467
|
|
|
471
468
|
it "updates both headers if both are configured" do
|
|
472
469
|
Configuration.default do |config|
|
|
473
470
|
config.csp = {
|
|
474
|
-
default_src: %w(enforced.com)
|
|
471
|
+
default_src: %w(enforced.com),
|
|
472
|
+
script_src: %w('self')
|
|
475
473
|
}
|
|
476
474
|
config.csp_report_only = {
|
|
477
|
-
default_src: %w(reportonly.com)
|
|
475
|
+
default_src: %w(reportonly.com),
|
|
476
|
+
script_src: %w('self')
|
|
478
477
|
}
|
|
479
478
|
end
|
|
480
479
|
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
|
|
481
480
|
|
|
482
481
|
hash = SecureHeaders.header_hash_for(request)
|
|
483
|
-
expect(hash[
|
|
484
|
-
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")
|
|
485
484
|
end
|
|
486
485
|
|
|
487
486
|
end
|
|
@@ -493,7 +492,7 @@ module SecureHeaders
|
|
|
493
492
|
it "validates your hsts config upon configuration" do
|
|
494
493
|
expect do
|
|
495
494
|
Configuration.default do |config|
|
|
496
|
-
config.hsts =
|
|
495
|
+
config.hsts = "lol"
|
|
497
496
|
end
|
|
498
497
|
end.to raise_error(STSConfigError)
|
|
499
498
|
end
|
|
@@ -501,7 +500,7 @@ module SecureHeaders
|
|
|
501
500
|
it "validates your csp config upon configuration" do
|
|
502
501
|
expect do
|
|
503
502
|
Configuration.default do |config|
|
|
504
|
-
config.csp = { ContentSecurityPolicy::DEFAULT_SRC =>
|
|
503
|
+
config.csp = { ContentSecurityPolicy::DEFAULT_SRC => "123456" }
|
|
505
504
|
end
|
|
506
505
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
507
506
|
end
|
|
@@ -509,7 +508,7 @@ module SecureHeaders
|
|
|
509
508
|
it "raises errors for unknown directives" do
|
|
510
509
|
expect do
|
|
511
510
|
Configuration.default do |config|
|
|
512
|
-
config.csp = { made_up_directive:
|
|
511
|
+
config.csp = { made_up_directive: "123456" }
|
|
513
512
|
end
|
|
514
513
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
515
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",
|
|
@@ -34,6 +33,7 @@ def expect_default_values(hash)
|
|
|
34
33
|
expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE)
|
|
35
34
|
expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE)
|
|
36
35
|
expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to be_nil
|
|
36
|
+
expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
module SecureHeaders
|
|
@@ -49,15 +49,3 @@ end
|
|
|
49
49
|
def reset_config
|
|
50
50
|
SecureHeaders::Configuration.clear_configurations
|
|
51
51
|
end
|
|
52
|
-
|
|
53
|
-
def capture_warning
|
|
54
|
-
begin
|
|
55
|
-
old_stderr = $stderr
|
|
56
|
-
$stderr = StringIO.new
|
|
57
|
-
yield
|
|
58
|
-
result = $stderr.string
|
|
59
|
-
ensure
|
|
60
|
-
$stderr = old_stderr
|
|
61
|
-
end
|
|
62
|
-
result
|
|
63
|
-
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.
|