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