secure_headers 3.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE.md +41 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +20 -0
- data/.rspec +2 -0
- data/.rubocop.yml +3 -0
- data/.ruby-version +1 -1
- data/.travis.yml +8 -4
- data/CHANGELOG.md +144 -1
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +41 -0
- data/Gemfile +9 -4
- data/Guardfile +1 -0
- data/LICENSE +4 -199
- data/README.md +73 -344
- data/Rakefile +22 -18
- data/docs/HPKP.md +17 -0
- data/docs/cookies.md +64 -0
- data/docs/hashes.md +64 -0
- data/docs/named_overrides_and_appends.md +107 -0
- data/docs/per_action_configuration.md +133 -0
- data/docs/sinatra.md +25 -0
- data/lib/secure_headers/configuration.rb +116 -42
- data/lib/secure_headers/hash_helper.rb +2 -1
- data/lib/secure_headers/headers/clear_site_data.rb +55 -0
- data/lib/secure_headers/headers/content_security_policy.rb +128 -39
- data/lib/secure_headers/headers/content_security_policy_config.rb +162 -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 +158 -68
- data/lib/secure_headers/headers/public_key_pins.rb +5 -4
- data/lib/secure_headers/headers/referrer_policy.rb +37 -0
- data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
- data/lib/secure_headers/headers/x_content_type_options.rb +2 -1
- data/lib/secure_headers/headers/x_download_options.rb +3 -2
- data/lib/secure_headers/headers/x_frame_options.rb +2 -1
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +3 -2
- data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
- data/lib/secure_headers/middleware.rb +16 -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 +15 -8
- data/lib/secure_headers.rb +139 -55
- data/lib/tasks/tasks.rake +7 -5
- data/secure_headers.gemspec +13 -3
- data/spec/lib/secure_headers/configuration_spec.rb +13 -12
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +87 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +89 -15
- 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 +80 -42
- data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +73 -0
- 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 +55 -17
- data/spec/lib/secure_headers/view_helpers_spec.rb +21 -8
- data/spec/lib/secure_headers_spec.rb +330 -58
- data/spec/spec_helper.rb +17 -25
- data/upgrading-to-3-0.md +13 -10
- data/upgrading-to-4-0.md +55 -0
- metadata +34 -7
|
@@ -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
|
|
@@ -16,19 +17,30 @@ module SecureHeaders
|
|
|
16
17
|
|
|
17
18
|
it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do
|
|
18
19
|
expect do
|
|
19
|
-
SecureHeaders.opt_out_of_header(request,
|
|
20
|
+
SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY)
|
|
20
21
|
end.to raise_error(Configuration::NotYetConfiguredError)
|
|
21
22
|
end
|
|
22
23
|
|
|
24
|
+
it "raises and ArgumentError when referencing an override that has not been set" do
|
|
25
|
+
expect do
|
|
26
|
+
Configuration.default
|
|
27
|
+
SecureHeaders.use_secure_headers_override(request, :missing)
|
|
28
|
+
end.to raise_error(ArgumentError)
|
|
29
|
+
end
|
|
30
|
+
|
|
23
31
|
describe "#header_hash_for" do
|
|
24
32
|
it "allows you to opt out of individual headers via API" do
|
|
25
|
-
Configuration.default
|
|
26
|
-
|
|
33
|
+
Configuration.default do |config|
|
|
34
|
+
config.csp = { default_src: %w('self'), script_src: %w('self')}
|
|
35
|
+
config.csp_report_only = config.csp
|
|
36
|
+
end
|
|
37
|
+
SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY)
|
|
38
|
+
SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY)
|
|
27
39
|
SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY)
|
|
28
40
|
hash = SecureHeaders.header_hash_for(request)
|
|
29
|
-
expect(hash[
|
|
30
|
-
expect(hash[
|
|
31
|
-
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
|
|
32
44
|
end
|
|
33
45
|
|
|
34
46
|
it "Carries options over when using overrides" do
|
|
@@ -43,13 +55,27 @@ module SecureHeaders
|
|
|
43
55
|
|
|
44
56
|
SecureHeaders.use_secure_headers_override(request, :api)
|
|
45
57
|
hash = SecureHeaders.header_hash_for(request)
|
|
46
|
-
expect(hash[
|
|
47
|
-
expect(hash[
|
|
48
|
-
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
|
|
49
61
|
end
|
|
50
62
|
|
|
51
63
|
it "allows you to opt out entirely" do
|
|
52
|
-
|
|
64
|
+
# configure the disabled-by-default headers to ensure they also do not get set
|
|
65
|
+
Configuration.default do |config|
|
|
66
|
+
config.csp = { default_src: ["example.com"], script_src: %w('self') }
|
|
67
|
+
config.csp_report_only = config.csp
|
|
68
|
+
config.hpkp = {
|
|
69
|
+
report_only: false,
|
|
70
|
+
max_age: 10000000,
|
|
71
|
+
include_subdomains: true,
|
|
72
|
+
report_uri: "https://report-uri.io/example-hpkp",
|
|
73
|
+
pins: [
|
|
74
|
+
{sha256: "abc"},
|
|
75
|
+
{sha256: "123"}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
end
|
|
53
79
|
SecureHeaders.opt_out_of_all_protection(request)
|
|
54
80
|
hash = SecureHeaders.header_hash_for(request)
|
|
55
81
|
ALL_HEADER_CLASSES.each do |klass|
|
|
@@ -75,27 +101,29 @@ module SecureHeaders
|
|
|
75
101
|
SecureHeaders.override_content_security_policy_directives(request, default_src: %w(https:), script_src: %w('self'))
|
|
76
102
|
|
|
77
103
|
hash = SecureHeaders.header_hash_for(request)
|
|
78
|
-
expect(hash[
|
|
104
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; script-src 'self'")
|
|
79
105
|
expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN)
|
|
80
106
|
end
|
|
81
107
|
|
|
82
108
|
it "produces a UA-specific CSP when overriding (and busting the cache)" do
|
|
83
|
-
|
|
109
|
+
Configuration.default do |config|
|
|
84
110
|
config.csp = {
|
|
85
111
|
default_src: %w('self'),
|
|
86
|
-
|
|
87
|
-
|
|
112
|
+
script_src: %w('self'),
|
|
113
|
+
child_src: %w('self')
|
|
88
114
|
}
|
|
89
115
|
end
|
|
90
116
|
firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox]))
|
|
91
117
|
|
|
92
118
|
# append an unsupported directive
|
|
93
|
-
SecureHeaders.override_content_security_policy_directives(firefox_request, plugin_types: %w(flash))
|
|
119
|
+
SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(flash)})
|
|
94
120
|
# append a supported directive
|
|
95
|
-
SecureHeaders.override_content_security_policy_directives(firefox_request, script_src: %w('self'))
|
|
121
|
+
SecureHeaders.override_content_security_policy_directives(firefox_request, {script_src: %w('self')})
|
|
96
122
|
|
|
97
123
|
hash = SecureHeaders.header_hash_for(firefox_request)
|
|
98
|
-
|
|
124
|
+
|
|
125
|
+
# child-src is translated to frame-src
|
|
126
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
|
|
99
127
|
end
|
|
100
128
|
|
|
101
129
|
it "produces a hash of headers with default config" do
|
|
@@ -118,10 +146,10 @@ module SecureHeaders
|
|
|
118
146
|
config.hpkp = {
|
|
119
147
|
max_age: 1_000_000,
|
|
120
148
|
include_subdomains: true,
|
|
121
|
-
report_uri:
|
|
149
|
+
report_uri: "//example.com/uri-directive",
|
|
122
150
|
pins: [
|
|
123
|
-
{ sha256:
|
|
124
|
-
{ sha256:
|
|
151
|
+
{ sha256: "abc" },
|
|
152
|
+
{ sha256: "123" }
|
|
125
153
|
]
|
|
126
154
|
}
|
|
127
155
|
end
|
|
@@ -130,6 +158,10 @@ module SecureHeaders
|
|
|
130
158
|
end
|
|
131
159
|
|
|
132
160
|
context "content security policy" do
|
|
161
|
+
let(:chrome_request) {
|
|
162
|
+
Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
|
|
163
|
+
}
|
|
164
|
+
|
|
133
165
|
it "appends a value to csp directive" do
|
|
134
166
|
Configuration.default do |config|
|
|
135
167
|
config.csp = {
|
|
@@ -140,66 +172,97 @@ module SecureHeaders
|
|
|
140
172
|
|
|
141
173
|
SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
|
|
142
174
|
hash = SecureHeaders.header_hash_for(request)
|
|
143
|
-
expect(hash[
|
|
175
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
|
|
144
176
|
end
|
|
145
177
|
|
|
146
|
-
it "
|
|
178
|
+
it "child-src and frame-src must match" do
|
|
147
179
|
Configuration.default do |config|
|
|
148
180
|
config.csp = {
|
|
149
|
-
default_src: %w('self')
|
|
181
|
+
default_src: %w('self'),
|
|
182
|
+
frame_src: %w(frame_src.com),
|
|
183
|
+
script_src: %w('self')
|
|
150
184
|
}
|
|
151
185
|
end
|
|
152
186
|
|
|
153
|
-
|
|
187
|
+
SecureHeaders.append_content_security_policy_directives(chrome_request, child_src: %w(child_src.com))
|
|
154
188
|
|
|
155
|
-
|
|
156
|
-
|
|
189
|
+
expect {
|
|
190
|
+
SecureHeaders.header_hash_for(chrome_request)
|
|
191
|
+
}.to raise_error(ArgumentError)
|
|
192
|
+
end
|
|
157
193
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
194
|
+
it "supports named appends" do
|
|
195
|
+
Configuration.default do |config|
|
|
196
|
+
config.csp = {
|
|
197
|
+
default_src: %w('self'),
|
|
198
|
+
script_src: %w('self')
|
|
199
|
+
}
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
Configuration.named_append(:moar_default_sources) do |request|
|
|
203
|
+
{ default_src: %w(https:), style_src: %w('self')}
|
|
204
|
+
end
|
|
161
205
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
206
|
+
Configuration.named_append(:how_about_a_script_src_too) do |request|
|
|
207
|
+
{ script_src: %w('unsafe-inline')}
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
SecureHeaders.use_content_security_policy_named_append(request, :moar_default_sources)
|
|
211
|
+
SecureHeaders.use_content_security_policy_named_append(request, :how_about_a_script_src_too)
|
|
212
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
165
213
|
|
|
166
|
-
|
|
214
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; script-src 'self' 'unsafe-inline'; style-src 'self'")
|
|
167
215
|
end
|
|
168
216
|
|
|
169
|
-
it "
|
|
170
|
-
|
|
171
|
-
|
|
217
|
+
it "appends a nonce to a missing script-src value" do
|
|
218
|
+
Configuration.default do |config|
|
|
219
|
+
config.csp = {
|
|
220
|
+
default_src: %w('self'),
|
|
221
|
+
script_src: %w('self')
|
|
222
|
+
}
|
|
223
|
+
end
|
|
172
224
|
|
|
173
|
-
#
|
|
174
|
-
|
|
225
|
+
SecureHeaders.content_security_policy_script_nonce(request) # should add the value to the header
|
|
226
|
+
hash = SecureHeaders.header_hash_for(chrome_request)
|
|
227
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/\Adefault-src 'self'; script-src 'self' 'nonce-.*'\z/)
|
|
228
|
+
end
|
|
175
229
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
230
|
+
it "appends a hash to a missing script-src value" do
|
|
231
|
+
Configuration.default do |config|
|
|
232
|
+
config.csp = {
|
|
233
|
+
default_src: %w('self'),
|
|
234
|
+
script_src: %w('self')
|
|
235
|
+
}
|
|
236
|
+
end
|
|
179
237
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
end
|
|
184
|
-
end.to raise_error(Configuration::IllegalPolicyModificationError)
|
|
238
|
+
SecureHeaders.append_content_security_policy_directives(request, script_src: %w('sha256-abc123'))
|
|
239
|
+
hash = SecureHeaders.header_hash_for(chrome_request)
|
|
240
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/\Adefault-src 'self'; script-src 'self' 'sha256-abc123'\z/)
|
|
185
241
|
end
|
|
186
242
|
|
|
187
243
|
it "overrides individual directives" do
|
|
188
244
|
Configuration.default do |config|
|
|
189
245
|
config.csp = {
|
|
190
|
-
default_src: %w('self')
|
|
246
|
+
default_src: %w('self'),
|
|
247
|
+
script_src: %w('self')
|
|
191
248
|
}
|
|
192
249
|
end
|
|
193
250
|
SecureHeaders.override_content_security_policy_directives(request, default_src: %w('none'))
|
|
194
251
|
hash = SecureHeaders.header_hash_for(request)
|
|
195
|
-
expect(hash[
|
|
252
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'none'; script-src 'self'")
|
|
196
253
|
end
|
|
197
254
|
|
|
198
255
|
it "overrides non-existant directives" do
|
|
199
|
-
Configuration.default
|
|
256
|
+
Configuration.default do |config|
|
|
257
|
+
config.csp = {
|
|
258
|
+
default_src: %w(https:),
|
|
259
|
+
script_src: %w('self')
|
|
260
|
+
}
|
|
261
|
+
end
|
|
200
262
|
SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL])
|
|
201
263
|
hash = SecureHeaders.header_hash_for(request)
|
|
202
|
-
expect(hash[
|
|
264
|
+
expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to be_nil
|
|
265
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; img-src data:; script-src 'self'")
|
|
203
266
|
end
|
|
204
267
|
|
|
205
268
|
it "does not append a nonce when the browser does not support it" do
|
|
@@ -212,9 +275,9 @@ module SecureHeaders
|
|
|
212
275
|
end
|
|
213
276
|
|
|
214
277
|
safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5]))
|
|
215
|
-
|
|
278
|
+
SecureHeaders.content_security_policy_script_nonce(safari_request)
|
|
216
279
|
hash = SecureHeaders.header_hash_for(safari_request)
|
|
217
|
-
expect(hash[
|
|
280
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
|
|
218
281
|
end
|
|
219
282
|
|
|
220
283
|
it "appends a nonce to the script-src when used" do
|
|
@@ -226,7 +289,6 @@ module SecureHeaders
|
|
|
226
289
|
}
|
|
227
290
|
end
|
|
228
291
|
|
|
229
|
-
chrome_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
|
|
230
292
|
nonce = SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
231
293
|
|
|
232
294
|
# simulate the nonce being used multiple times in a request:
|
|
@@ -235,7 +297,193 @@ module SecureHeaders
|
|
|
235
297
|
SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
236
298
|
|
|
237
299
|
hash = SecureHeaders.header_hash_for(chrome_request)
|
|
238
|
-
expect(hash[
|
|
300
|
+
expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'")
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
it "uses a nonce for safari 10+" do
|
|
304
|
+
Configuration.default do |config|
|
|
305
|
+
config.csp = {
|
|
306
|
+
default_src: %w('self'),
|
|
307
|
+
script_src: %w(mycdn.com)
|
|
308
|
+
}
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari10]))
|
|
312
|
+
nonce = SecureHeaders.content_security_policy_script_nonce(safari_request)
|
|
313
|
+
hash = SecureHeaders.header_hash_for(safari_request)
|
|
314
|
+
expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'")
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
it "does not support the deprecated `report_only: true` format" do
|
|
318
|
+
expect {
|
|
319
|
+
Configuration.default do |config|
|
|
320
|
+
config.csp = {
|
|
321
|
+
default_src: %w('self'),
|
|
322
|
+
report_only: true
|
|
323
|
+
}
|
|
324
|
+
end
|
|
325
|
+
}.to raise_error(ArgumentError)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
it "Raises an error if csp_report_only is used with `report_only: false`" do
|
|
329
|
+
expect do
|
|
330
|
+
Configuration.default do |config|
|
|
331
|
+
config.csp_report_only = {
|
|
332
|
+
default_src: %w('self'),
|
|
333
|
+
script_src: %w('self'),
|
|
334
|
+
report_only: false
|
|
335
|
+
}
|
|
336
|
+
end
|
|
337
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
context "setting two headers" do
|
|
341
|
+
before(:each) do
|
|
342
|
+
Configuration.default do |config|
|
|
343
|
+
config.csp = {
|
|
344
|
+
default_src: %w('self'),
|
|
345
|
+
script_src: %w('self')
|
|
346
|
+
}
|
|
347
|
+
config.csp_report_only = config.csp
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
it "sets identical values when the configs are the same" do
|
|
352
|
+
Configuration.default do |config|
|
|
353
|
+
config.csp = {
|
|
354
|
+
default_src: %w('self'),
|
|
355
|
+
script_src: %w('self')
|
|
356
|
+
}
|
|
357
|
+
config.csp_report_only = {
|
|
358
|
+
default_src: %w('self'),
|
|
359
|
+
script_src: %w('self')
|
|
360
|
+
}
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
364
|
+
expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'")
|
|
365
|
+
expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'")
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
it "sets different headers when the configs are different" do
|
|
369
|
+
Configuration.default do |config|
|
|
370
|
+
config.csp = {
|
|
371
|
+
default_src: %w('self'),
|
|
372
|
+
script_src: %w('self')
|
|
373
|
+
}
|
|
374
|
+
config.csp_report_only = config.csp.merge({script_src: %w(foo.com)})
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
378
|
+
expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'")
|
|
379
|
+
expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' foo.com")
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
it "allows you to opt-out of enforced CSP" do
|
|
383
|
+
Configuration.default do |config|
|
|
384
|
+
config.csp = SecureHeaders::OPT_OUT
|
|
385
|
+
config.csp_report_only = {
|
|
386
|
+
default_src: %w('self'),
|
|
387
|
+
script_src: %w('self')
|
|
388
|
+
}
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
392
|
+
expect(hash["Content-Security-Policy"]).to be_nil
|
|
393
|
+
expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'")
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
it "allows appending to the enforced policy" do
|
|
397
|
+
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced)
|
|
398
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
399
|
+
expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com")
|
|
400
|
+
expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'")
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
it "allows appending to the report only policy" do
|
|
404
|
+
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only)
|
|
405
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
406
|
+
expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'")
|
|
407
|
+
expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com")
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
it "allows appending to both policies" do
|
|
411
|
+
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both)
|
|
412
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
413
|
+
expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com")
|
|
414
|
+
expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com")
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
it "allows overriding the enforced policy" do
|
|
418
|
+
SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced)
|
|
419
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
420
|
+
expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src anothercdn.com")
|
|
421
|
+
expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'")
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
it "allows overriding the report only policy" do
|
|
425
|
+
SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only)
|
|
426
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
427
|
+
expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'")
|
|
428
|
+
expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src anothercdn.com")
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
it "allows overriding both policies" do
|
|
432
|
+
SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both)
|
|
433
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
434
|
+
expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src anothercdn.com")
|
|
435
|
+
expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src anothercdn.com")
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
context "when inferring which config to modify" do
|
|
439
|
+
it "updates the enforced header when configured" do
|
|
440
|
+
Configuration.default do |config|
|
|
441
|
+
config.csp = {
|
|
442
|
+
default_src: %w('self'),
|
|
443
|
+
script_src: %w('self')
|
|
444
|
+
}
|
|
445
|
+
end
|
|
446
|
+
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
|
|
447
|
+
|
|
448
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
449
|
+
expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com")
|
|
450
|
+
expect(hash["Content-Security-Policy-Report-Only"]).to be_nil
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
it "updates the report only header when configured" do
|
|
454
|
+
Configuration.default do |config|
|
|
455
|
+
config.csp = OPT_OUT
|
|
456
|
+
config.csp_report_only = {
|
|
457
|
+
default_src: %w('self'),
|
|
458
|
+
script_src: %w('self')
|
|
459
|
+
}
|
|
460
|
+
end
|
|
461
|
+
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
|
|
462
|
+
|
|
463
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
464
|
+
expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com")
|
|
465
|
+
expect(hash["Content-Security-Policy"]).to be_nil
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
it "updates both headers if both are configured" do
|
|
469
|
+
Configuration.default do |config|
|
|
470
|
+
config.csp = {
|
|
471
|
+
default_src: %w(enforced.com),
|
|
472
|
+
script_src: %w('self')
|
|
473
|
+
}
|
|
474
|
+
config.csp_report_only = {
|
|
475
|
+
default_src: %w(reportonly.com),
|
|
476
|
+
script_src: %w('self')
|
|
477
|
+
}
|
|
478
|
+
end
|
|
479
|
+
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
|
|
480
|
+
|
|
481
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
482
|
+
expect(hash["Content-Security-Policy"]).to eq("default-src enforced.com; script-src 'self' anothercdn.com")
|
|
483
|
+
expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src reportonly.com; script-src 'self' anothercdn.com")
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
end
|
|
239
487
|
end
|
|
240
488
|
end
|
|
241
489
|
end
|
|
@@ -244,7 +492,7 @@ module SecureHeaders
|
|
|
244
492
|
it "validates your hsts config upon configuration" do
|
|
245
493
|
expect do
|
|
246
494
|
Configuration.default do |config|
|
|
247
|
-
config.hsts =
|
|
495
|
+
config.hsts = "lol"
|
|
248
496
|
end
|
|
249
497
|
end.to raise_error(STSConfigError)
|
|
250
498
|
end
|
|
@@ -252,7 +500,7 @@ module SecureHeaders
|
|
|
252
500
|
it "validates your csp config upon configuration" do
|
|
253
501
|
expect do
|
|
254
502
|
Configuration.default do |config|
|
|
255
|
-
config.csp = {
|
|
503
|
+
config.csp = { ContentSecurityPolicy::DEFAULT_SRC => "123456" }
|
|
256
504
|
end
|
|
257
505
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
258
506
|
end
|
|
@@ -260,7 +508,7 @@ module SecureHeaders
|
|
|
260
508
|
it "raises errors for unknown directives" do
|
|
261
509
|
expect do
|
|
262
510
|
Configuration.default do |config|
|
|
263
|
-
config.csp = { made_up_directive:
|
|
511
|
+
config.csp = { made_up_directive: "123456" }
|
|
264
512
|
end
|
|
265
513
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
266
514
|
end
|
|
@@ -281,6 +529,14 @@ module SecureHeaders
|
|
|
281
529
|
end.to raise_error(XContentTypeOptionsConfigError)
|
|
282
530
|
end
|
|
283
531
|
|
|
532
|
+
it "validates your clear site data config upon configuration" do
|
|
533
|
+
expect do
|
|
534
|
+
Configuration.default do |config|
|
|
535
|
+
config.clear_site_data = 1
|
|
536
|
+
end
|
|
537
|
+
end.to raise_error(ClearSiteDataConfigError)
|
|
538
|
+
end
|
|
539
|
+
|
|
284
540
|
it "validates your x_xss config upon configuration" do
|
|
285
541
|
expect do
|
|
286
542
|
Configuration.default do |config|
|
|
@@ -305,6 +561,14 @@ module SecureHeaders
|
|
|
305
561
|
end.to raise_error(XPCDPConfigError)
|
|
306
562
|
end
|
|
307
563
|
|
|
564
|
+
it "validates your referrer_policy config upon configuration" do
|
|
565
|
+
expect do
|
|
566
|
+
Configuration.default do |config|
|
|
567
|
+
config.referrer_policy = "lol"
|
|
568
|
+
end
|
|
569
|
+
end.to raise_error(ReferrerPolicyConfigError)
|
|
570
|
+
end
|
|
571
|
+
|
|
308
572
|
it "validates your hpkp config upon configuration" do
|
|
309
573
|
expect do
|
|
310
574
|
Configuration.default do |config|
|
|
@@ -312,6 +576,14 @@ module SecureHeaders
|
|
|
312
576
|
end
|
|
313
577
|
end.to raise_error(PublicKeyPinsConfigError)
|
|
314
578
|
end
|
|
579
|
+
|
|
580
|
+
it "validates your cookies config upon configuration" do
|
|
581
|
+
expect do
|
|
582
|
+
Configuration.default do |config|
|
|
583
|
+
config.cookies = { secure: "lol" }
|
|
584
|
+
end
|
|
585
|
+
end.to raise_error(CookiesConfigError)
|
|
586
|
+
end
|
|
315
587
|
end
|
|
316
588
|
end
|
|
317
589
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -1,35 +1,39 @@
|
|
|
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
|
|
|
13
12
|
USER_AGENTS = {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
edge: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246",
|
|
14
|
+
firefox: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1",
|
|
15
|
+
firefox46: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:46.0) Gecko/20100101 Firefox/46.0",
|
|
16
|
+
chrome: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.56 Safari/536.5",
|
|
17
|
+
ie: "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)",
|
|
18
|
+
opera: "Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00",
|
|
18
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",
|
|
19
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",
|
|
20
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",
|
|
21
22
|
safari5_1: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10",
|
|
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"
|
|
23
|
+
safari6: "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/536.30.1 (KHTML like Gecko) Version/6.0.5 Safari/536.30.1",
|
|
24
|
+
safari10: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/602.2.11 (KHTML, like Gecko) Version/10.0.1 Safari/602.2.11"
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
def expect_default_values(hash)
|
|
26
|
-
expect(hash[SecureHeaders::
|
|
28
|
+
expect(hash[SecureHeaders::ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'")
|
|
27
29
|
expect(hash[SecureHeaders::XFrameOptions::HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::DEFAULT_VALUE)
|
|
28
30
|
expect(hash[SecureHeaders::XDownloadOptions::HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::DEFAULT_VALUE)
|
|
29
31
|
expect(hash[SecureHeaders::StrictTransportSecurity::HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::DEFAULT_VALUE)
|
|
30
32
|
expect(hash[SecureHeaders::XXssProtection::HEADER_NAME]).to eq(SecureHeaders::XXssProtection::DEFAULT_VALUE)
|
|
31
33
|
expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE)
|
|
32
34
|
expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE)
|
|
35
|
+
expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to be_nil
|
|
36
|
+
expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil
|
|
33
37
|
end
|
|
34
38
|
|
|
35
39
|
module SecureHeaders
|
|
@@ -45,15 +49,3 @@ end
|
|
|
45
49
|
def reset_config
|
|
46
50
|
SecureHeaders::Configuration.clear_configurations
|
|
47
51
|
end
|
|
48
|
-
|
|
49
|
-
def capture_warning
|
|
50
|
-
begin
|
|
51
|
-
old_stderr = $stderr
|
|
52
|
-
$stderr = StringIO.new
|
|
53
|
-
yield
|
|
54
|
-
result = $stderr.string
|
|
55
|
-
ensure
|
|
56
|
-
$stderr = old_stderr
|
|
57
|
-
end
|
|
58
|
-
result
|
|
59
|
-
end
|