secure_headers 3.0.3 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE.md +41 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +20 -0
- data/.gitignore +0 -9
- data/.rspec +2 -0
- data/.rubocop.yml +3 -0
- data/.ruby-version +1 -1
- data/.travis.yml +17 -5
- data/CHANGELOG.md +287 -0
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +41 -0
- data/Gemfile +13 -5
- data/Guardfile +1 -0
- data/LICENSE +4 -199
- data/README.md +85 -265
- 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 +188 -59
- data/lib/secure_headers/hash_helper.rb +11 -0
- data/lib/secure_headers/headers/clear_site_data.rb +55 -0
- data/lib/secure_headers/headers/content_security_policy.rb +136 -325
- data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
- data/lib/secure_headers/headers/cookie.rb +144 -0
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
- data/lib/secure_headers/headers/policy_management.rb +410 -0
- 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 +3 -2
- 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 +44 -0
- data/lib/secure_headers/railtie.rb +11 -6
- data/lib/secure_headers/utils/cookies_config.rb +95 -0
- data/lib/secure_headers/view_helper.rb +76 -5
- data/lib/secure_headers.rb +159 -99
- data/lib/tasks/tasks.rake +83 -0
- data/secure_headers.gemspec +13 -3
- data/spec/lib/secure_headers/configuration_spec.rb +24 -9
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +87 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +88 -189
- data/spec/lib/secure_headers/headers/cookie_spec.rb +182 -0
- data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +228 -0
- 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 +6 -5
- 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 +96 -6
- data/spec/lib/secure_headers/view_helpers_spec.rb +138 -0
- data/spec/lib/secure_headers_spec.rb +407 -58
- data/spec/spec_helper.rb +19 -12
- data/upgrading-to-3-0.md +13 -9
- data/upgrading-to-4-0.md +55 -0
- metadata +45 -9
- data/lib/secure_headers/padrino.rb +0 -13
- data/travis.sh +0 -10
|
@@ -1,58 +1,93 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "spec_helper"
|
|
2
3
|
|
|
3
4
|
module SecureHeaders
|
|
4
5
|
describe SecureHeaders do
|
|
5
|
-
example_hpkp_config = {
|
|
6
|
-
max_age: 1_000_000,
|
|
7
|
-
include_subdomains: true,
|
|
8
|
-
report_uri: '//example.com/uri-directive',
|
|
9
|
-
pins: [
|
|
10
|
-
{ sha256: 'abc' },
|
|
11
|
-
{ sha256: '123' }
|
|
12
|
-
]
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
example_hpkp_config_value = %(max-age=1000000; pin-sha256="abc"; pin-sha256="123"; report-uri="//example.com/uri-directive"; includeSubDomains)
|
|
16
|
-
|
|
17
6
|
before(:each) do
|
|
18
7
|
reset_config
|
|
19
|
-
@request = Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on")
|
|
20
8
|
end
|
|
21
9
|
|
|
10
|
+
let(:request) { Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on") }
|
|
11
|
+
|
|
22
12
|
it "raises a NotYetConfiguredError if default has not been set" do
|
|
23
13
|
expect do
|
|
24
|
-
SecureHeaders.header_hash_for(
|
|
14
|
+
SecureHeaders.header_hash_for(request)
|
|
25
15
|
end.to raise_error(Configuration::NotYetConfiguredError)
|
|
26
16
|
end
|
|
27
17
|
|
|
28
18
|
it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do
|
|
29
19
|
expect do
|
|
30
|
-
SecureHeaders.opt_out_of_header(
|
|
20
|
+
SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY)
|
|
31
21
|
end.to raise_error(Configuration::NotYetConfiguredError)
|
|
32
22
|
end
|
|
33
23
|
|
|
34
|
-
|
|
35
|
-
|
|
24
|
+
it "raises and ArgumentError when referencing an override that has not been set" do
|
|
25
|
+
expect do
|
|
36
26
|
Configuration.default
|
|
37
|
-
SecureHeaders.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
SecureHeaders.use_secure_headers_override(request, :missing)
|
|
28
|
+
end.to raise_error(ArgumentError)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe "#header_hash_for" do
|
|
32
|
+
it "allows you to opt out of individual headers via API" do
|
|
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)
|
|
39
|
+
SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY)
|
|
40
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
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
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "Carries options over when using overrides" do
|
|
47
|
+
Configuration.default do |config|
|
|
48
|
+
config.x_download_options = OPT_OUT
|
|
49
|
+
config.x_permitted_cross_domain_policies = OPT_OUT
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
Configuration.override(:api) do |config|
|
|
53
|
+
config.x_frame_options = OPT_OUT
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
SecureHeaders.use_secure_headers_override(request, :api)
|
|
57
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
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
|
|
41
61
|
end
|
|
42
62
|
|
|
43
63
|
it "allows you to opt out entirely" do
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
79
|
+
SecureHeaders.opt_out_of_all_protection(request)
|
|
80
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
47
81
|
ALL_HEADER_CLASSES.each do |klass|
|
|
48
82
|
expect(hash[klass::CONFIG_KEY]).to be_nil
|
|
49
83
|
end
|
|
84
|
+
expect(hash.count).to eq(0)
|
|
50
85
|
end
|
|
51
86
|
|
|
52
87
|
it "allows you to override X-Frame-Options settings" do
|
|
53
88
|
Configuration.default
|
|
54
|
-
SecureHeaders.override_x_frame_options(
|
|
55
|
-
hash = SecureHeaders.header_hash_for(
|
|
89
|
+
SecureHeaders.override_x_frame_options(request, XFrameOptions::DENY)
|
|
90
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
56
91
|
expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::DENY)
|
|
57
92
|
end
|
|
58
93
|
|
|
@@ -62,17 +97,38 @@ module SecureHeaders
|
|
|
62
97
|
config.csp = OPT_OUT
|
|
63
98
|
end
|
|
64
99
|
|
|
65
|
-
SecureHeaders.override_x_frame_options(
|
|
66
|
-
SecureHeaders.override_content_security_policy_directives(
|
|
100
|
+
SecureHeaders.override_x_frame_options(request, XFrameOptions::SAMEORIGIN)
|
|
101
|
+
SecureHeaders.override_content_security_policy_directives(request, default_src: %w(https:), script_src: %w('self'))
|
|
67
102
|
|
|
68
|
-
hash = SecureHeaders.header_hash_for(
|
|
69
|
-
expect(hash[
|
|
103
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
104
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; script-src 'self'")
|
|
70
105
|
expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN)
|
|
71
106
|
end
|
|
72
107
|
|
|
108
|
+
it "produces a UA-specific CSP when overriding (and busting the cache)" do
|
|
109
|
+
Configuration.default do |config|
|
|
110
|
+
config.csp = {
|
|
111
|
+
default_src: %w('self'),
|
|
112
|
+
script_src: %w('self'),
|
|
113
|
+
child_src: %w('self')
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox]))
|
|
117
|
+
|
|
118
|
+
# append an unsupported directive
|
|
119
|
+
SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(flash)})
|
|
120
|
+
# append a supported directive
|
|
121
|
+
SecureHeaders.override_content_security_policy_directives(firefox_request, {script_src: %w('self')})
|
|
122
|
+
|
|
123
|
+
hash = SecureHeaders.header_hash_for(firefox_request)
|
|
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'")
|
|
127
|
+
end
|
|
128
|
+
|
|
73
129
|
it "produces a hash of headers with default config" do
|
|
74
130
|
Configuration.default
|
|
75
|
-
hash = SecureHeaders.header_hash_for(
|
|
131
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
76
132
|
expect_default_values(hash)
|
|
77
133
|
end
|
|
78
134
|
|
|
@@ -87,13 +143,25 @@ module SecureHeaders
|
|
|
87
143
|
it "does not set the HPKP header if request is over HTTP" do
|
|
88
144
|
plaintext_request = Rack::Request.new({})
|
|
89
145
|
Configuration.default do |config|
|
|
90
|
-
config.hpkp =
|
|
146
|
+
config.hpkp = {
|
|
147
|
+
max_age: 1_000_000,
|
|
148
|
+
include_subdomains: true,
|
|
149
|
+
report_uri: "//example.com/uri-directive",
|
|
150
|
+
pins: [
|
|
151
|
+
{ sha256: "abc" },
|
|
152
|
+
{ sha256: "123" }
|
|
153
|
+
]
|
|
154
|
+
}
|
|
91
155
|
end
|
|
92
156
|
|
|
93
157
|
expect(SecureHeaders.header_hash_for(plaintext_request)[PublicKeyPins::HEADER_NAME]).to be_nil
|
|
94
158
|
end
|
|
95
159
|
|
|
96
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
|
+
|
|
97
165
|
it "appends a value to csp directive" do
|
|
98
166
|
Configuration.default do |config|
|
|
99
167
|
config.csp = {
|
|
@@ -102,27 +170,99 @@ module SecureHeaders
|
|
|
102
170
|
}
|
|
103
171
|
end
|
|
104
172
|
|
|
105
|
-
SecureHeaders.append_content_security_policy_directives(
|
|
106
|
-
hash = SecureHeaders.header_hash_for(
|
|
107
|
-
expect(hash[
|
|
173
|
+
SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
|
|
174
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
175
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it "child-src and frame-src must match" do
|
|
179
|
+
Configuration.default do |config|
|
|
180
|
+
config.csp = {
|
|
181
|
+
default_src: %w('self'),
|
|
182
|
+
frame_src: %w(frame_src.com),
|
|
183
|
+
script_src: %w('self')
|
|
184
|
+
}
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
SecureHeaders.append_content_security_policy_directives(chrome_request, child_src: %w(child_src.com))
|
|
188
|
+
|
|
189
|
+
expect {
|
|
190
|
+
SecureHeaders.header_hash_for(chrome_request)
|
|
191
|
+
}.to raise_error(ArgumentError)
|
|
192
|
+
end
|
|
193
|
+
|
|
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
|
|
205
|
+
|
|
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)
|
|
213
|
+
|
|
214
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; script-src 'self' 'unsafe-inline'; style-src 'self'")
|
|
215
|
+
end
|
|
216
|
+
|
|
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
|
|
224
|
+
|
|
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
|
|
229
|
+
|
|
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
|
|
237
|
+
|
|
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/)
|
|
108
241
|
end
|
|
109
242
|
|
|
110
243
|
it "overrides individual directives" do
|
|
111
244
|
Configuration.default do |config|
|
|
112
245
|
config.csp = {
|
|
113
|
-
default_src: %w('self')
|
|
246
|
+
default_src: %w('self'),
|
|
247
|
+
script_src: %w('self')
|
|
114
248
|
}
|
|
115
249
|
end
|
|
116
|
-
SecureHeaders.override_content_security_policy_directives(
|
|
117
|
-
hash = SecureHeaders.header_hash_for(
|
|
118
|
-
expect(hash[
|
|
250
|
+
SecureHeaders.override_content_security_policy_directives(request, default_src: %w('none'))
|
|
251
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
252
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'none'; script-src 'self'")
|
|
119
253
|
end
|
|
120
254
|
|
|
121
255
|
it "overrides non-existant directives" do
|
|
122
|
-
Configuration.default
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
256
|
+
Configuration.default do |config|
|
|
257
|
+
config.csp = {
|
|
258
|
+
default_src: %w(https:),
|
|
259
|
+
script_src: %w('self')
|
|
260
|
+
}
|
|
261
|
+
end
|
|
262
|
+
SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL])
|
|
263
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
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'")
|
|
126
266
|
end
|
|
127
267
|
|
|
128
268
|
it "does not append a nonce when the browser does not support it" do
|
|
@@ -134,10 +274,10 @@ module SecureHeaders
|
|
|
134
274
|
}
|
|
135
275
|
end
|
|
136
276
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
hash = SecureHeaders.header_hash_for(
|
|
140
|
-
expect(hash[
|
|
277
|
+
safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5]))
|
|
278
|
+
SecureHeaders.content_security_policy_script_nonce(safari_request)
|
|
279
|
+
hash = SecureHeaders.header_hash_for(safari_request)
|
|
280
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
|
|
141
281
|
end
|
|
142
282
|
|
|
143
283
|
it "appends a nonce to the script-src when used" do
|
|
@@ -149,16 +289,201 @@ module SecureHeaders
|
|
|
149
289
|
}
|
|
150
290
|
end
|
|
151
291
|
|
|
152
|
-
|
|
153
|
-
nonce = SecureHeaders.content_security_policy_script_nonce(request)
|
|
292
|
+
nonce = SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
154
293
|
|
|
155
294
|
# simulate the nonce being used multiple times in a request:
|
|
156
|
-
SecureHeaders.content_security_policy_script_nonce(
|
|
157
|
-
SecureHeaders.content_security_policy_script_nonce(
|
|
158
|
-
SecureHeaders.content_security_policy_script_nonce(
|
|
295
|
+
SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
296
|
+
SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
297
|
+
SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
159
298
|
|
|
160
|
-
hash = SecureHeaders.header_hash_for(
|
|
161
|
-
expect(hash[
|
|
299
|
+
hash = SecureHeaders.header_hash_for(chrome_request)
|
|
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
|
|
162
487
|
end
|
|
163
488
|
end
|
|
164
489
|
end
|
|
@@ -167,7 +492,7 @@ module SecureHeaders
|
|
|
167
492
|
it "validates your hsts config upon configuration" do
|
|
168
493
|
expect do
|
|
169
494
|
Configuration.default do |config|
|
|
170
|
-
config.hsts =
|
|
495
|
+
config.hsts = "lol"
|
|
171
496
|
end
|
|
172
497
|
end.to raise_error(STSConfigError)
|
|
173
498
|
end
|
|
@@ -175,7 +500,7 @@ module SecureHeaders
|
|
|
175
500
|
it "validates your csp config upon configuration" do
|
|
176
501
|
expect do
|
|
177
502
|
Configuration.default do |config|
|
|
178
|
-
config.csp = {
|
|
503
|
+
config.csp = { ContentSecurityPolicy::DEFAULT_SRC => "123456" }
|
|
179
504
|
end
|
|
180
505
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
181
506
|
end
|
|
@@ -183,7 +508,7 @@ module SecureHeaders
|
|
|
183
508
|
it "raises errors for unknown directives" do
|
|
184
509
|
expect do
|
|
185
510
|
Configuration.default do |config|
|
|
186
|
-
config.csp = { made_up_directive:
|
|
511
|
+
config.csp = { made_up_directive: "123456" }
|
|
187
512
|
end
|
|
188
513
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
189
514
|
end
|
|
@@ -204,6 +529,14 @@ module SecureHeaders
|
|
|
204
529
|
end.to raise_error(XContentTypeOptionsConfigError)
|
|
205
530
|
end
|
|
206
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
|
+
|
|
207
540
|
it "validates your x_xss config upon configuration" do
|
|
208
541
|
expect do
|
|
209
542
|
Configuration.default do |config|
|
|
@@ -228,6 +561,14 @@ module SecureHeaders
|
|
|
228
561
|
end.to raise_error(XPCDPConfigError)
|
|
229
562
|
end
|
|
230
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
|
+
|
|
231
572
|
it "validates your hpkp config upon configuration" do
|
|
232
573
|
expect do
|
|
233
574
|
Configuration.default do |config|
|
|
@@ -235,6 +576,14 @@ module SecureHeaders
|
|
|
235
576
|
end
|
|
236
577
|
end.to raise_error(PublicKeyPinsConfigError)
|
|
237
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
|
|
238
587
|
end
|
|
239
588
|
end
|
|
240
589
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -1,32 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
require
|
|
3
|
-
require
|
|
4
|
-
require
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "rubygems"
|
|
3
|
+
require "rspec"
|
|
4
|
+
require "rack"
|
|
5
|
+
require "coveralls"
|
|
6
|
+
Coveralls.wear!
|
|
7
|
+
|
|
8
|
+
require File.join(File.dirname(__FILE__), "..", "lib", "secure_headers")
|
|
5
9
|
|
|
6
|
-
require File.join(File.dirname(__FILE__), '..', 'lib', 'secure_headers')
|
|
7
10
|
|
|
8
|
-
ENV["RAILS_ENV"] = "test"
|
|
9
11
|
|
|
10
12
|
USER_AGENTS = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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",
|
|
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",
|
|
15
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",
|
|
16
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",
|
|
17
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",
|
|
18
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",
|
|
19
|
-
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"
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
def expect_default_values(hash)
|
|
23
|
-
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'")
|
|
24
29
|
expect(hash[SecureHeaders::XFrameOptions::HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::DEFAULT_VALUE)
|
|
25
30
|
expect(hash[SecureHeaders::XDownloadOptions::HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::DEFAULT_VALUE)
|
|
26
31
|
expect(hash[SecureHeaders::StrictTransportSecurity::HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::DEFAULT_VALUE)
|
|
27
32
|
expect(hash[SecureHeaders::XXssProtection::HEADER_NAME]).to eq(SecureHeaders::XXssProtection::DEFAULT_VALUE)
|
|
28
33
|
expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE)
|
|
29
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
|
|
30
37
|
end
|
|
31
38
|
|
|
32
39
|
module SecureHeaders
|