secure_headers 3.4.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/.rspec +2 -0
- data/.rubocop.yml +3 -0
- data/.ruby-version +1 -1
- data/.travis.yml +8 -4
- data/CHANGELOG.md +113 -1
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +41 -0
- data/Gemfile +8 -4
- data/Guardfile +1 -0
- data/LICENSE +4 -199
- data/README.md +63 -333
- 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 +91 -44
- 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 +110 -51
- 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 +143 -69
- data/lib/secure_headers/headers/public_key_pins.rb +5 -4
- data/lib/secure_headers/headers/referrer_policy.rb +5 -1
- 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 +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 +12 -3
- data/lib/secure_headers.rb +137 -55
- data/lib/tasks/tasks.rake +2 -1
- 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 +59 -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 +78 -40
- data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +22 -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 +39 -19
- data/spec/lib/secure_headers/view_helpers_spec.rb +21 -8
- data/spec/lib/secure_headers_spec.rb +304 -56
- data/spec/spec_helper.rb +13 -24
- data/upgrading-to-4-0.md +55 -0
- metadata +29 -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,7 +17,7 @@ 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
|
|
|
@@ -29,13 +30,17 @@ module SecureHeaders
|
|
|
29
30
|
|
|
30
31
|
describe "#header_hash_for" do
|
|
31
32
|
it "allows you to opt out of individual headers via API" do
|
|
32
|
-
Configuration.default
|
|
33
|
-
|
|
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)
|
|
34
39
|
SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY)
|
|
35
40
|
hash = SecureHeaders.header_hash_for(request)
|
|
36
|
-
expect(hash[
|
|
37
|
-
expect(hash[
|
|
38
|
-
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
|
|
39
44
|
end
|
|
40
45
|
|
|
41
46
|
it "Carries options over when using overrides" do
|
|
@@ -50,13 +55,27 @@ module SecureHeaders
|
|
|
50
55
|
|
|
51
56
|
SecureHeaders.use_secure_headers_override(request, :api)
|
|
52
57
|
hash = SecureHeaders.header_hash_for(request)
|
|
53
|
-
expect(hash[
|
|
54
|
-
expect(hash[
|
|
55
|
-
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
|
|
56
61
|
end
|
|
57
62
|
|
|
58
63
|
it "allows you to opt out entirely" do
|
|
59
|
-
|
|
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
|
|
60
79
|
SecureHeaders.opt_out_of_all_protection(request)
|
|
61
80
|
hash = SecureHeaders.header_hash_for(request)
|
|
62
81
|
ALL_HEADER_CLASSES.each do |klass|
|
|
@@ -82,28 +101,29 @@ module SecureHeaders
|
|
|
82
101
|
SecureHeaders.override_content_security_policy_directives(request, default_src: %w(https:), script_src: %w('self'))
|
|
83
102
|
|
|
84
103
|
hash = SecureHeaders.header_hash_for(request)
|
|
85
|
-
expect(hash[
|
|
104
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; script-src 'self'")
|
|
86
105
|
expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN)
|
|
87
106
|
end
|
|
88
107
|
|
|
89
108
|
it "produces a UA-specific CSP when overriding (and busting the cache)" do
|
|
90
|
-
|
|
109
|
+
Configuration.default do |config|
|
|
91
110
|
config.csp = {
|
|
92
111
|
default_src: %w('self'),
|
|
112
|
+
script_src: %w('self'),
|
|
93
113
|
child_src: %w('self')
|
|
94
114
|
}
|
|
95
115
|
end
|
|
96
116
|
firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox]))
|
|
97
117
|
|
|
98
118
|
# append an unsupported directive
|
|
99
|
-
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)})
|
|
100
120
|
# append a supported directive
|
|
101
|
-
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')})
|
|
102
122
|
|
|
103
123
|
hash = SecureHeaders.header_hash_for(firefox_request)
|
|
104
124
|
|
|
105
125
|
# child-src is translated to frame-src
|
|
106
|
-
expect(hash[
|
|
126
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
|
|
107
127
|
end
|
|
108
128
|
|
|
109
129
|
it "produces a hash of headers with default config" do
|
|
@@ -126,10 +146,10 @@ module SecureHeaders
|
|
|
126
146
|
config.hpkp = {
|
|
127
147
|
max_age: 1_000_000,
|
|
128
148
|
include_subdomains: true,
|
|
129
|
-
report_uri:
|
|
149
|
+
report_uri: "//example.com/uri-directive",
|
|
130
150
|
pins: [
|
|
131
|
-
{ sha256:
|
|
132
|
-
{ sha256:
|
|
151
|
+
{ sha256: "abc" },
|
|
152
|
+
{ sha256: "123" }
|
|
133
153
|
]
|
|
134
154
|
}
|
|
135
155
|
end
|
|
@@ -138,6 +158,10 @@ module SecureHeaders
|
|
|
138
158
|
end
|
|
139
159
|
|
|
140
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
|
+
|
|
141
165
|
it "appends a value to csp directive" do
|
|
142
166
|
Configuration.default do |config|
|
|
143
167
|
config.csp = {
|
|
@@ -148,66 +172,97 @@ module SecureHeaders
|
|
|
148
172
|
|
|
149
173
|
SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
|
|
150
174
|
hash = SecureHeaders.header_hash_for(request)
|
|
151
|
-
expect(hash[
|
|
175
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
|
|
152
176
|
end
|
|
153
177
|
|
|
154
|
-
it "
|
|
178
|
+
it "child-src and frame-src must match" do
|
|
155
179
|
Configuration.default do |config|
|
|
156
180
|
config.csp = {
|
|
157
|
-
default_src: %w('self')
|
|
181
|
+
default_src: %w('self'),
|
|
182
|
+
frame_src: %w(frame_src.com),
|
|
183
|
+
script_src: %w('self')
|
|
158
184
|
}
|
|
159
185
|
end
|
|
160
186
|
|
|
161
|
-
|
|
187
|
+
SecureHeaders.append_content_security_policy_directives(chrome_request, child_src: %w(child_src.com))
|
|
162
188
|
|
|
163
|
-
|
|
164
|
-
|
|
189
|
+
expect {
|
|
190
|
+
SecureHeaders.header_hash_for(chrome_request)
|
|
191
|
+
}.to raise_error(ArgumentError)
|
|
192
|
+
end
|
|
165
193
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
169
201
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
202
|
+
Configuration.named_append(:moar_default_sources) do |request|
|
|
203
|
+
{ default_src: %w(https:), style_src: %w('self')}
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
Configuration.named_append(:how_about_a_script_src_too) do |request|
|
|
207
|
+
{ script_src: %w('unsafe-inline')}
|
|
208
|
+
end
|
|
173
209
|
|
|
174
|
-
SecureHeaders.
|
|
210
|
+
SecureHeaders.use_content_security_policy_named_append(request, :moar_default_sources)
|
|
211
|
+
SecureHeaders.use_content_security_policy_named_append(request, :how_about_a_script_src_too)
|
|
212
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
213
|
+
|
|
214
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; script-src 'self' 'unsafe-inline'; style-src 'self'")
|
|
175
215
|
end
|
|
176
216
|
|
|
177
|
-
it "
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
180
224
|
|
|
181
|
-
#
|
|
182
|
-
|
|
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
|
|
183
229
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
187
237
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
end
|
|
192
|
-
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/)
|
|
193
241
|
end
|
|
194
242
|
|
|
195
243
|
it "overrides individual directives" do
|
|
196
244
|
Configuration.default do |config|
|
|
197
245
|
config.csp = {
|
|
198
|
-
default_src: %w('self')
|
|
246
|
+
default_src: %w('self'),
|
|
247
|
+
script_src: %w('self')
|
|
199
248
|
}
|
|
200
249
|
end
|
|
201
250
|
SecureHeaders.override_content_security_policy_directives(request, default_src: %w('none'))
|
|
202
251
|
hash = SecureHeaders.header_hash_for(request)
|
|
203
|
-
expect(hash[
|
|
252
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'none'; script-src 'self'")
|
|
204
253
|
end
|
|
205
254
|
|
|
206
255
|
it "overrides non-existant directives" do
|
|
207
|
-
Configuration.default
|
|
256
|
+
Configuration.default do |config|
|
|
257
|
+
config.csp = {
|
|
258
|
+
default_src: %w(https:),
|
|
259
|
+
script_src: %w('self')
|
|
260
|
+
}
|
|
261
|
+
end
|
|
208
262
|
SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL])
|
|
209
263
|
hash = SecureHeaders.header_hash_for(request)
|
|
210
|
-
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'")
|
|
211
266
|
end
|
|
212
267
|
|
|
213
268
|
it "does not append a nonce when the browser does not support it" do
|
|
@@ -220,9 +275,9 @@ module SecureHeaders
|
|
|
220
275
|
end
|
|
221
276
|
|
|
222
277
|
safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5]))
|
|
223
|
-
|
|
278
|
+
SecureHeaders.content_security_policy_script_nonce(safari_request)
|
|
224
279
|
hash = SecureHeaders.header_hash_for(safari_request)
|
|
225
|
-
expect(hash[
|
|
280
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
|
|
226
281
|
end
|
|
227
282
|
|
|
228
283
|
it "appends a nonce to the script-src when used" do
|
|
@@ -234,7 +289,6 @@ module SecureHeaders
|
|
|
234
289
|
}
|
|
235
290
|
end
|
|
236
291
|
|
|
237
|
-
chrome_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
|
|
238
292
|
nonce = SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
239
293
|
|
|
240
294
|
# simulate the nonce being used multiple times in a request:
|
|
@@ -243,7 +297,193 @@ module SecureHeaders
|
|
|
243
297
|
SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
244
298
|
|
|
245
299
|
hash = SecureHeaders.header_hash_for(chrome_request)
|
|
246
|
-
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
|
|
247
487
|
end
|
|
248
488
|
end
|
|
249
489
|
end
|
|
@@ -252,7 +492,7 @@ module SecureHeaders
|
|
|
252
492
|
it "validates your hsts config upon configuration" do
|
|
253
493
|
expect do
|
|
254
494
|
Configuration.default do |config|
|
|
255
|
-
config.hsts =
|
|
495
|
+
config.hsts = "lol"
|
|
256
496
|
end
|
|
257
497
|
end.to raise_error(STSConfigError)
|
|
258
498
|
end
|
|
@@ -260,7 +500,7 @@ module SecureHeaders
|
|
|
260
500
|
it "validates your csp config upon configuration" do
|
|
261
501
|
expect do
|
|
262
502
|
Configuration.default do |config|
|
|
263
|
-
config.csp = {
|
|
503
|
+
config.csp = { ContentSecurityPolicy::DEFAULT_SRC => "123456" }
|
|
264
504
|
end
|
|
265
505
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
266
506
|
end
|
|
@@ -268,7 +508,7 @@ module SecureHeaders
|
|
|
268
508
|
it "raises errors for unknown directives" do
|
|
269
509
|
expect do
|
|
270
510
|
Configuration.default do |config|
|
|
271
|
-
config.csp = { made_up_directive:
|
|
511
|
+
config.csp = { made_up_directive: "123456" }
|
|
272
512
|
end
|
|
273
513
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
274
514
|
end
|
|
@@ -289,6 +529,14 @@ module SecureHeaders
|
|
|
289
529
|
end.to raise_error(XContentTypeOptionsConfigError)
|
|
290
530
|
end
|
|
291
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
|
+
|
|
292
540
|
it "validates your x_xss config upon configuration" do
|
|
293
541
|
expect do
|
|
294
542
|
Configuration.default do |config|
|
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,18 +13,19 @@ 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",
|
|
23
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",
|
|
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"
|
|
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"
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
def expect_default_values(hash)
|
|
28
|
-
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'")
|
|
29
29
|
expect(hash[SecureHeaders::XFrameOptions::HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::DEFAULT_VALUE)
|
|
30
30
|
expect(hash[SecureHeaders::XDownloadOptions::HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::DEFAULT_VALUE)
|
|
31
31
|
expect(hash[SecureHeaders::StrictTransportSecurity::HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::DEFAULT_VALUE)
|
|
@@ -33,6 +33,7 @@ def expect_default_values(hash)
|
|
|
33
33
|
expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE)
|
|
34
34
|
expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE)
|
|
35
35
|
expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to be_nil
|
|
36
|
+
expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
module SecureHeaders
|
|
@@ -48,15 +49,3 @@ end
|
|
|
48
49
|
def reset_config
|
|
49
50
|
SecureHeaders::Configuration.clear_configurations
|
|
50
51
|
end
|
|
51
|
-
|
|
52
|
-
def capture_warning
|
|
53
|
-
begin
|
|
54
|
-
old_stderr = $stderr
|
|
55
|
-
$stderr = StringIO.new
|
|
56
|
-
yield
|
|
57
|
-
result = $stderr.string
|
|
58
|
-
ensure
|
|
59
|
-
$stderr = old_stderr
|
|
60
|
-
end
|
|
61
|
-
result
|
|
62
|
-
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.
|