secure_headers 5.1.0 → 6.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/.travis.yml +8 -4
- data/CHANGELOG.md +3 -3
- data/Gemfile +1 -1
- data/README.md +2 -2
- data/docs/upgrading-to-6-0.md +50 -0
- data/lib/secure_headers/configuration.rb +114 -164
- data/lib/secure_headers/headers/clear_site_data.rb +1 -3
- data/lib/secure_headers/headers/content_security_policy.rb +8 -74
- data/lib/secure_headers/headers/content_security_policy_config.rb +3 -13
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +2 -3
- data/lib/secure_headers/headers/policy_management.rb +14 -65
- data/lib/secure_headers/headers/public_key_pins.rb +2 -3
- data/lib/secure_headers/headers/referrer_policy.rb +2 -2
- data/lib/secure_headers/headers/strict_transport_security.rb +2 -2
- data/lib/secure_headers/headers/x_content_type_options.rb +2 -2
- data/lib/secure_headers/headers/x_download_options.rb +2 -2
- data/lib/secure_headers/headers/x_frame_options.rb +1 -2
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +2 -2
- data/lib/secure_headers/headers/x_xss_protection.rb +3 -3
- data/lib/secure_headers/view_helper.rb +9 -8
- data/lib/secure_headers.rb +14 -78
- data/secure_headers.gemspec +1 -2
- data/spec/lib/secure_headers/configuration_spec.rb +15 -70
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +2 -70
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +35 -9
- data/spec/lib/secure_headers/middleware_spec.rb +7 -1
- data/spec/lib/secure_headers/view_helpers_spec.rb +29 -0
- data/spec/lib/secure_headers_spec.rb +38 -76
- data/spec/spec_helper.rb +7 -3
- metadata +3 -16
|
@@ -5,88 +5,33 @@ module SecureHeaders
|
|
|
5
5
|
describe Configuration do
|
|
6
6
|
before(:each) do
|
|
7
7
|
reset_config
|
|
8
|
-
Configuration.default
|
|
9
8
|
end
|
|
10
9
|
|
|
11
10
|
it "has a default config" do
|
|
12
|
-
expect(Configuration.
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
it "warns when using deprecated internal-ish #get API" do
|
|
16
|
-
expect(Kernel).to receive(:warn).once.with(/`#get` is deprecated/)
|
|
17
|
-
Configuration.get(Configuration::DEFAULT_CONFIG)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
it "has an 'noop' config" do
|
|
21
|
-
expect(Configuration.get(Configuration::NOOP_CONFIGURATION, internal: true)).to_not be_nil
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
it "precomputes headers upon creation" do
|
|
25
|
-
default_config = Configuration.get(Configuration::DEFAULT_CONFIG, internal: true)
|
|
26
|
-
header_hash = default_config.cached_headers.each_with_object({}) do |(key, value), hash|
|
|
27
|
-
header_name, header_value = if key == :csp
|
|
28
|
-
value["Chrome"]
|
|
29
|
-
else
|
|
30
|
-
value
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
hash[header_name] = header_value
|
|
34
|
-
end
|
|
35
|
-
expect_default_values(header_hash)
|
|
11
|
+
expect(Configuration.default).to_not be_nil
|
|
36
12
|
end
|
|
37
13
|
|
|
38
|
-
it "
|
|
39
|
-
Configuration.
|
|
40
|
-
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
config = Configuration.get(:test_override, internal: true)
|
|
44
|
-
noop = Configuration.get(Configuration::NOOP_CONFIGURATION, internal: true)
|
|
45
|
-
[:csp, :csp_report_only, :cookies].each do |key|
|
|
46
|
-
expect(config.send(key)).to eq(noop.send(key))
|
|
47
|
-
end
|
|
14
|
+
it "has an 'noop' override" do
|
|
15
|
+
Configuration.default
|
|
16
|
+
expect(Configuration.overrides(Configuration::NOOP_OVERRIDE)).to_not be_nil
|
|
48
17
|
end
|
|
49
18
|
|
|
50
|
-
it "
|
|
51
|
-
Configuration.
|
|
52
|
-
|
|
19
|
+
it "dup results in a copy of the default config" do
|
|
20
|
+
Configuration.default
|
|
21
|
+
original_configuration = Configuration.send(:default_config)
|
|
22
|
+
configuration = Configuration.dup
|
|
23
|
+
expect(original_configuration).not_to be(configuration)
|
|
24
|
+
Configuration::CONFIG_ATTRIBUTES.each do |attr|
|
|
25
|
+
expect(original_configuration.send(attr)).to eq(configuration.send(attr))
|
|
53
26
|
end
|
|
54
|
-
|
|
55
|
-
expect(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).cached_headers).to_not eq(Configuration.get(:test_override, internal: true).cached_headers)
|
|
56
27
|
end
|
|
57
28
|
|
|
58
|
-
it "stores an override
|
|
29
|
+
it "stores an override" do
|
|
59
30
|
Configuration.override(:test_override) do |config|
|
|
60
31
|
config.x_frame_options = "DENY"
|
|
61
32
|
end
|
|
62
33
|
|
|
63
|
-
expect(Configuration.
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
it "deep dup's config values when overriding so the original cannot be modified" do
|
|
67
|
-
Configuration.override(:override) do |config|
|
|
68
|
-
config.csp[:default_src] << "'self'"
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
default = Configuration.get(Configuration::DEFAULT_CONFIG, internal: true)
|
|
72
|
-
override = Configuration.get(:override, internal: true)
|
|
73
|
-
|
|
74
|
-
expect(override.csp.directive_value(:default_src)).not_to be(default.csp.directive_value(:default_src))
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
it "allows you to override an override" do
|
|
78
|
-
Configuration.override(:override) do |config|
|
|
79
|
-
config.csp = { default_src: %w('self'), script_src: %w('self')}
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
Configuration.override(:second_override, :override) do |config|
|
|
83
|
-
config.csp = config.csp.merge(script_src: %w(example.org))
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
original_override = Configuration.get(:override, internal: true)
|
|
87
|
-
expect(original_override.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self'))
|
|
88
|
-
override_config = Configuration.get(:second_override, internal: true)
|
|
89
|
-
expect(override_config.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self' example.org))
|
|
34
|
+
expect(Configuration.overrides(:test_override)).to_not be_nil
|
|
90
35
|
end
|
|
91
36
|
|
|
92
37
|
it "deprecates the secure_cookies configuration" do
|
|
@@ -106,7 +51,7 @@ module SecureHeaders
|
|
|
106
51
|
config.cookies = OPT_OUT
|
|
107
52
|
end
|
|
108
53
|
|
|
109
|
-
config = Configuration.
|
|
54
|
+
config = Configuration.dup
|
|
110
55
|
expect(config.cookies).to eq(OPT_OUT)
|
|
111
56
|
end
|
|
112
57
|
|
|
@@ -115,7 +60,7 @@ module SecureHeaders
|
|
|
115
60
|
config.cookies = {httponly: true, secure: true, samesite: {lax: false}}
|
|
116
61
|
end
|
|
117
62
|
|
|
118
|
-
config = Configuration.
|
|
63
|
+
config = Configuration.dup
|
|
119
64
|
expect(config.cookies).to eq({httponly: true, secure: true, samesite: {lax: false}})
|
|
120
65
|
end
|
|
121
66
|
end
|
|
@@ -28,11 +28,6 @@ module SecureHeaders
|
|
|
28
28
|
expect(ContentSecurityPolicy.new.value).to eq("default-src https:; form-action 'self'; img-src https: data: 'self'; object-src 'none'; script-src https:; style-src 'self' 'unsafe-inline' https:")
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
it "deprecates and escapes semicolons in directive source lists" do
|
|
32
|
-
expect(Kernel).to receive(:warn).with("frame_ancestors contains a ; in 'google.com;script-src *;.;' which will raise an error in future versions. It has been replaced with a blank space.")
|
|
33
|
-
expect(ContentSecurityPolicy.new(frame_ancestors: %w(https://google.com;script-src https://*;.;)).value).to eq("frame-ancestors google.com script-src * .")
|
|
34
|
-
end
|
|
35
|
-
|
|
36
31
|
it "discards 'none' values if any other source expressions are present" do
|
|
37
32
|
csp = ContentSecurityPolicy.new(default_opts.merge(child_src: %w('self' 'none')))
|
|
38
33
|
expect(csp.value).not_to include("'none'")
|
|
@@ -121,72 +116,9 @@ module SecureHeaders
|
|
|
121
116
|
ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
|
|
122
117
|
end
|
|
123
118
|
|
|
124
|
-
it "raises an error when child-src and frame-src are supplied but are not equal" do
|
|
125
|
-
expect {
|
|
126
|
-
ContentSecurityPolicy.new(default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)).value
|
|
127
|
-
}.to raise_error(ArgumentError)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
119
|
it "supports strict-dynamic" do
|
|
131
|
-
csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}
|
|
132
|
-
expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456'")
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
context "browser sniffing" do
|
|
136
|
-
let (:complex_opts) do
|
|
137
|
-
(ContentSecurityPolicy::ALL_DIRECTIVES - [:frame_src]).each_with_object({}) do |directive, hash|
|
|
138
|
-
hash[directive] = ["#{directive.to_s.gsub("_", "-")}.com"]
|
|
139
|
-
end.merge({
|
|
140
|
-
block_all_mixed_content: true,
|
|
141
|
-
upgrade_insecure_requests: true,
|
|
142
|
-
script_src: %w(script-src.com),
|
|
143
|
-
script_nonce: 123456,
|
|
144
|
-
sandbox: %w(allow-forms),
|
|
145
|
-
plugin_types: %w(application/pdf)
|
|
146
|
-
})
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
it "does not filter any directives for Chrome" do
|
|
150
|
-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
|
|
151
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
it "does not filter any directives for Opera" do
|
|
155
|
-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
|
|
156
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
|
|
160
|
-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
|
|
161
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
|
|
165
|
-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
|
|
166
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for Edge" do
|
|
170
|
-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge])
|
|
171
|
-
expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do
|
|
175
|
-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
|
|
176
|
-
expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, nonce sources, and hash sources for safari 10 and higher" do
|
|
180
|
-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10])
|
|
181
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; report-uri report-uri.com")
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
it "falls back to standard Firefox defaults when the useragent version is not present" do
|
|
185
|
-
ua = USER_AGENTS[:firefox].dup
|
|
186
|
-
allow(ua).to receive(:version).and_return(nil)
|
|
187
|
-
policy = ContentSecurityPolicy.new(complex_opts, ua)
|
|
188
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
189
|
-
end
|
|
120
|
+
csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456})
|
|
121
|
+
expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456' 'unsafe-inline'")
|
|
190
122
|
end
|
|
191
123
|
end
|
|
192
124
|
end
|
|
@@ -3,6 +3,11 @@ require "spec_helper"
|
|
|
3
3
|
|
|
4
4
|
module SecureHeaders
|
|
5
5
|
describe PolicyManagement do
|
|
6
|
+
before(:each) do
|
|
7
|
+
reset_config
|
|
8
|
+
Configuration.default
|
|
9
|
+
end
|
|
10
|
+
|
|
6
11
|
let (:default_opts) do
|
|
7
12
|
{
|
|
8
13
|
default_src: %w(https:),
|
|
@@ -18,7 +23,7 @@ module SecureHeaders
|
|
|
18
23
|
# (pulled from README)
|
|
19
24
|
config = {
|
|
20
25
|
# "meta" values. these will shape the header, but the values are not included in the header.
|
|
21
|
-
report_only:
|
|
26
|
+
report_only: false,
|
|
22
27
|
preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
|
|
23
28
|
|
|
24
29
|
# directive values: these values will directly translate into source directives
|
|
@@ -142,9 +147,24 @@ module SecureHeaders
|
|
|
142
147
|
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(plugin_types: ["application/pdf"])))
|
|
143
148
|
end.to_not raise_error
|
|
144
149
|
end
|
|
150
|
+
|
|
151
|
+
it "doesn't allow report_only to be set in a non-report-only config" do
|
|
152
|
+
expect do
|
|
153
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_only: true)))
|
|
154
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it "allows report_only to be set in a report-only config" do
|
|
158
|
+
expect do
|
|
159
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyReportOnlyConfig.new(default_opts.merge(report_only: true)))
|
|
160
|
+
end.to_not raise_error
|
|
161
|
+
end
|
|
145
162
|
end
|
|
146
163
|
|
|
147
164
|
describe "#combine_policies" do
|
|
165
|
+
before(:each) do
|
|
166
|
+
reset_config
|
|
167
|
+
end
|
|
148
168
|
it "combines the default-src value with the override if the directive was unconfigured" do
|
|
149
169
|
Configuration.default do |config|
|
|
150
170
|
config.csp = {
|
|
@@ -152,7 +172,8 @@ module SecureHeaders
|
|
|
152
172
|
script_src: %w('self'),
|
|
153
173
|
}
|
|
154
174
|
end
|
|
155
|
-
|
|
175
|
+
default_policy = Configuration.dup
|
|
176
|
+
combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, style_src: %w(anothercdn.com))
|
|
156
177
|
csp = ContentSecurityPolicy.new(combined_config)
|
|
157
178
|
expect(csp.name).to eq(ContentSecurityPolicyConfig::HEADER_NAME)
|
|
158
179
|
expect(csp.value).to eq("default-src https:; script-src 'self'; style-src https: anothercdn.com")
|
|
@@ -167,8 +188,9 @@ module SecureHeaders
|
|
|
167
188
|
}.freeze
|
|
168
189
|
end
|
|
169
190
|
report_uri = "https://report-uri.io/asdf"
|
|
170
|
-
|
|
171
|
-
|
|
191
|
+
default_policy = Configuration.dup
|
|
192
|
+
combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_uri: [report_uri])
|
|
193
|
+
csp = ContentSecurityPolicy.new(combined_config)
|
|
172
194
|
expect(csp.value).to include("report-uri #{report_uri}")
|
|
173
195
|
end
|
|
174
196
|
|
|
@@ -183,7 +205,8 @@ module SecureHeaders
|
|
|
183
205
|
non_default_source_additions = ContentSecurityPolicy::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash|
|
|
184
206
|
hash[directive] = %w("http://example.org)
|
|
185
207
|
end
|
|
186
|
-
|
|
208
|
+
default_policy = Configuration.dup
|
|
209
|
+
combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, non_default_source_additions)
|
|
187
210
|
|
|
188
211
|
ContentSecurityPolicy::NON_FETCH_SOURCES.each do |directive|
|
|
189
212
|
expect(combined_config[directive]).to eq(%w("http://example.org))
|
|
@@ -198,8 +221,9 @@ module SecureHeaders
|
|
|
198
221
|
report_only: false
|
|
199
222
|
}
|
|
200
223
|
end
|
|
201
|
-
|
|
202
|
-
|
|
224
|
+
default_policy = Configuration.dup
|
|
225
|
+
combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_only: true)
|
|
226
|
+
csp = ContentSecurityPolicy.new(combined_config)
|
|
203
227
|
expect(csp.name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME)
|
|
204
228
|
end
|
|
205
229
|
|
|
@@ -211,7 +235,8 @@ module SecureHeaders
|
|
|
211
235
|
block_all_mixed_content: false
|
|
212
236
|
}
|
|
213
237
|
end
|
|
214
|
-
|
|
238
|
+
default_policy = Configuration.dup
|
|
239
|
+
combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, block_all_mixed_content: true)
|
|
215
240
|
csp = ContentSecurityPolicy.new(combined_config)
|
|
216
241
|
expect(csp.value).to eq("default-src https:; block-all-mixed-content; script-src 'self'")
|
|
217
242
|
end
|
|
@@ -220,8 +245,9 @@ module SecureHeaders
|
|
|
220
245
|
Configuration.default do |config|
|
|
221
246
|
config.csp = OPT_OUT
|
|
222
247
|
end
|
|
248
|
+
default_policy = Configuration.dup
|
|
223
249
|
expect do
|
|
224
|
-
ContentSecurityPolicy.combine_policies(
|
|
250
|
+
ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, script_src: %w(anothercdn.com))
|
|
225
251
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
226
252
|
end
|
|
227
253
|
end
|
|
@@ -16,6 +16,7 @@ module SecureHeaders
|
|
|
16
16
|
|
|
17
17
|
it "warns if the hpkp report-uri host is the same as the current host" do
|
|
18
18
|
report_host = "report-uri.io"
|
|
19
|
+
reset_config
|
|
19
20
|
Configuration.default do |config|
|
|
20
21
|
config.hpkp = {
|
|
21
22
|
max_age: 10000000,
|
|
@@ -50,12 +51,14 @@ module SecureHeaders
|
|
|
50
51
|
end
|
|
51
52
|
request = Rack::Request.new({})
|
|
52
53
|
SecureHeaders.use_secure_headers_override(request, "my_custom_config")
|
|
53
|
-
expect(request.env[SECURE_HEADERS_CONFIG]).to be(Configuration.get("my_custom_config", internal: true))
|
|
54
54
|
_, env = middleware.call request.env
|
|
55
55
|
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match("example.org")
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
context "cookies" do
|
|
59
|
+
before(:each) do
|
|
60
|
+
reset_config
|
|
61
|
+
end
|
|
59
62
|
context "cookies should be flagged" do
|
|
60
63
|
it "flags cookies as secure" do
|
|
61
64
|
Configuration.default { |config| config.cookies = {secure: true, httponly: OPT_OUT, samesite: OPT_OUT} }
|
|
@@ -87,6 +90,9 @@ module SecureHeaders
|
|
|
87
90
|
end
|
|
88
91
|
|
|
89
92
|
context "cookies" do
|
|
93
|
+
before(:each) do
|
|
94
|
+
reset_config
|
|
95
|
+
end
|
|
90
96
|
it "flags cookies from configuration" do
|
|
91
97
|
Configuration.default { |config| config.cookies = { secure: true, httponly: true, samesite: { lax: true} } }
|
|
92
98
|
request = Rack::Request.new("HTTPS" => "on")
|
|
@@ -97,6 +97,12 @@ TEMPLATE
|
|
|
97
97
|
end
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
class MessageWithConflictingMethod < Message
|
|
101
|
+
def content_security_policy_nonce
|
|
102
|
+
"rails-nonce"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
100
106
|
module SecureHeaders
|
|
101
107
|
describe ViewHelpers do
|
|
102
108
|
let(:app) { lambda { |env| [200, env, "app"] } }
|
|
@@ -105,6 +111,7 @@ module SecureHeaders
|
|
|
105
111
|
let(:filename) { "app/views/asdfs/index.html.erb" }
|
|
106
112
|
|
|
107
113
|
before(:all) do
|
|
114
|
+
reset_config
|
|
108
115
|
Configuration.default do |config|
|
|
109
116
|
config.csp = {
|
|
110
117
|
default_src: %w('self'),
|
|
@@ -158,5 +165,27 @@ module SecureHeaders
|
|
|
158
165
|
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/)
|
|
159
166
|
end
|
|
160
167
|
end
|
|
168
|
+
|
|
169
|
+
it "avoids calling content_security_policy_nonce internally" do
|
|
170
|
+
begin
|
|
171
|
+
allow(SecureRandom).to receive(:base64).and_return("abc123")
|
|
172
|
+
|
|
173
|
+
expected_hash = "sha256-3/URElR9+3lvLIouavYD/vhoICSNKilh15CzI/nKqg8="
|
|
174
|
+
Configuration.instance_variable_set(:@script_hashes, filename => ["'#{expected_hash}'"])
|
|
175
|
+
expected_style_hash = "sha256-7oYK96jHg36D6BM042er4OfBnyUDTG3pH1L8Zso3aGc="
|
|
176
|
+
Configuration.instance_variable_set(:@style_hashes, filename => ["'#{expected_style_hash}'"])
|
|
177
|
+
|
|
178
|
+
# render erb that calls out to helpers.
|
|
179
|
+
MessageWithConflictingMethod.new(request).result
|
|
180
|
+
_, env = middleware.call request.env
|
|
181
|
+
|
|
182
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'#{Regexp.escape(expected_hash)}'/)
|
|
183
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/)
|
|
184
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/)
|
|
185
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/)
|
|
186
|
+
|
|
187
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).not_to match(/rails-nonce/)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
161
190
|
end
|
|
162
191
|
end
|
|
@@ -17,10 +17,17 @@ module SecureHeaders
|
|
|
17
17
|
|
|
18
18
|
it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do
|
|
19
19
|
expect do
|
|
20
|
-
SecureHeaders.opt_out_of_header(request,
|
|
20
|
+
SecureHeaders.opt_out_of_header(request, :csp)
|
|
21
21
|
end.to raise_error(Configuration::NotYetConfiguredError)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
it "raises a AlreadyConfiguredError if trying to configure and default has already been set " do
|
|
25
|
+
Configuration.default
|
|
26
|
+
expect do
|
|
27
|
+
Configuration.default
|
|
28
|
+
end.to raise_error(Configuration::AlreadyConfiguredError)
|
|
29
|
+
end
|
|
30
|
+
|
|
24
31
|
it "raises and ArgumentError when referencing an override that has not been set" do
|
|
25
32
|
expect do
|
|
26
33
|
Configuration.default
|
|
@@ -34,9 +41,9 @@ module SecureHeaders
|
|
|
34
41
|
config.csp = { default_src: %w('self'), script_src: %w('self')}
|
|
35
42
|
config.csp_report_only = config.csp
|
|
36
43
|
end
|
|
37
|
-
SecureHeaders.opt_out_of_header(request,
|
|
38
|
-
SecureHeaders.opt_out_of_header(request,
|
|
39
|
-
SecureHeaders.opt_out_of_header(request,
|
|
44
|
+
SecureHeaders.opt_out_of_header(request, :csp)
|
|
45
|
+
SecureHeaders.opt_out_of_header(request, :csp_report_only)
|
|
46
|
+
SecureHeaders.opt_out_of_header(request, :x_content_type_options)
|
|
40
47
|
hash = SecureHeaders.header_hash_for(request)
|
|
41
48
|
expect(hash["Content-Security-Policy-Report-Only"]).to be_nil
|
|
42
49
|
expect(hash["Content-Security-Policy"]).to be_nil
|
|
@@ -60,6 +67,24 @@ module SecureHeaders
|
|
|
60
67
|
expect(hash["X-Frame-Options"]).to be_nil
|
|
61
68
|
end
|
|
62
69
|
|
|
70
|
+
it "Overrides the current default config if default config changes during request" do
|
|
71
|
+
Configuration.default do |config|
|
|
72
|
+
config.x_frame_options = OPT_OUT
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Dynamically update the default config for this request
|
|
76
|
+
SecureHeaders.override_x_frame_options(request, "DENY")
|
|
77
|
+
|
|
78
|
+
Configuration.override(:dynamic_override) do |config|
|
|
79
|
+
config.x_content_type_options = "nosniff"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
SecureHeaders.use_secure_headers_override(request, :dynamic_override)
|
|
83
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
84
|
+
expect(hash["X-Content-Type-Options"]).to eq("nosniff")
|
|
85
|
+
expect(hash["X-Frame-Options"]).to eq("DENY")
|
|
86
|
+
end
|
|
87
|
+
|
|
63
88
|
it "allows you to opt out entirely" do
|
|
64
89
|
# configure the disabled-by-default headers to ensure they also do not get set
|
|
65
90
|
Configuration.default do |config|
|
|
@@ -78,9 +103,6 @@ module SecureHeaders
|
|
|
78
103
|
end
|
|
79
104
|
SecureHeaders.opt_out_of_all_protection(request)
|
|
80
105
|
hash = SecureHeaders.header_hash_for(request)
|
|
81
|
-
ALL_HEADER_CLASSES.each do |klass|
|
|
82
|
-
expect(hash[klass::CONFIG_KEY]).to be_nil
|
|
83
|
-
end
|
|
84
106
|
expect(hash.count).to eq(0)
|
|
85
107
|
end
|
|
86
108
|
|
|
@@ -105,27 +127,6 @@ module SecureHeaders
|
|
|
105
127
|
expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN)
|
|
106
128
|
end
|
|
107
129
|
|
|
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
|
-
|
|
129
130
|
it "produces a hash of headers with default config" do
|
|
130
131
|
Configuration.default
|
|
131
132
|
hash = SecureHeaders.header_hash_for(request)
|
|
@@ -175,22 +176,6 @@ module SecureHeaders
|
|
|
175
176
|
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
|
|
176
177
|
end
|
|
177
178
|
|
|
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
179
|
it "supports named appends" do
|
|
195
180
|
Configuration.default do |config|
|
|
196
181
|
config.csp = {
|
|
@@ -265,21 +250,6 @@ module SecureHeaders
|
|
|
265
250
|
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; img-src data:; script-src 'self'")
|
|
266
251
|
end
|
|
267
252
|
|
|
268
|
-
it "does not append a nonce when the browser does not support it" do
|
|
269
|
-
Configuration.default do |config|
|
|
270
|
-
config.csp = {
|
|
271
|
-
default_src: %w('self'),
|
|
272
|
-
script_src: %w(mycdn.com 'unsafe-inline'),
|
|
273
|
-
style_src: %w('self')
|
|
274
|
-
}
|
|
275
|
-
end
|
|
276
|
-
|
|
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'")
|
|
281
|
-
end
|
|
282
|
-
|
|
283
253
|
it "appends a nonce to the script-src when used" do
|
|
284
254
|
Configuration.default do |config|
|
|
285
255
|
config.csp = {
|
|
@@ -297,21 +267,7 @@ module SecureHeaders
|
|
|
297
267
|
SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
298
268
|
|
|
299
269
|
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}'")
|
|
270
|
+
expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}' 'unsafe-inline'; style-src 'self'")
|
|
315
271
|
end
|
|
316
272
|
|
|
317
273
|
it "does not support the deprecated `report_only: true` format" do
|
|
@@ -322,7 +278,7 @@ module SecureHeaders
|
|
|
322
278
|
report_only: true
|
|
323
279
|
}
|
|
324
280
|
end
|
|
325
|
-
}.to raise_error(
|
|
281
|
+
}.to raise_error(ContentSecurityPolicyConfigError)
|
|
326
282
|
end
|
|
327
283
|
|
|
328
284
|
it "Raises an error if csp_report_only is used with `report_only: false`" do
|
|
@@ -349,6 +305,7 @@ module SecureHeaders
|
|
|
349
305
|
end
|
|
350
306
|
|
|
351
307
|
it "sets identical values when the configs are the same" do
|
|
308
|
+
reset_config
|
|
352
309
|
Configuration.default do |config|
|
|
353
310
|
config.csp = {
|
|
354
311
|
default_src: %w('self'),
|
|
@@ -366,6 +323,7 @@ module SecureHeaders
|
|
|
366
323
|
end
|
|
367
324
|
|
|
368
325
|
it "sets different headers when the configs are different" do
|
|
326
|
+
reset_config
|
|
369
327
|
Configuration.default do |config|
|
|
370
328
|
config.csp = {
|
|
371
329
|
default_src: %w('self'),
|
|
@@ -376,10 +334,11 @@ module SecureHeaders
|
|
|
376
334
|
|
|
377
335
|
hash = SecureHeaders.header_hash_for(request)
|
|
378
336
|
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
|
|
337
|
+
expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src foo.com")
|
|
380
338
|
end
|
|
381
339
|
|
|
382
340
|
it "allows you to opt-out of enforced CSP" do
|
|
341
|
+
reset_config
|
|
383
342
|
Configuration.default do |config|
|
|
384
343
|
config.csp = SecureHeaders::OPT_OUT
|
|
385
344
|
config.csp_report_only = {
|
|
@@ -437,6 +396,7 @@ module SecureHeaders
|
|
|
437
396
|
|
|
438
397
|
context "when inferring which config to modify" do
|
|
439
398
|
it "updates the enforced header when configured" do
|
|
399
|
+
reset_config
|
|
440
400
|
Configuration.default do |config|
|
|
441
401
|
config.csp = {
|
|
442
402
|
default_src: %w('self'),
|
|
@@ -451,6 +411,7 @@ module SecureHeaders
|
|
|
451
411
|
end
|
|
452
412
|
|
|
453
413
|
it "updates the report only header when configured" do
|
|
414
|
+
reset_config
|
|
454
415
|
Configuration.default do |config|
|
|
455
416
|
config.csp = OPT_OUT
|
|
456
417
|
config.csp_report_only = {
|
|
@@ -466,6 +427,7 @@ module SecureHeaders
|
|
|
466
427
|
end
|
|
467
428
|
|
|
468
429
|
it "updates both headers if both are configured" do
|
|
430
|
+
reset_config
|
|
469
431
|
Configuration.default do |config|
|
|
470
432
|
config.csp = {
|
|
471
433
|
default_src: %w(enforced.com),
|
data/spec/spec_helper.rb
CHANGED
|
@@ -26,6 +26,7 @@ USER_AGENTS = {
|
|
|
26
26
|
|
|
27
27
|
def expect_default_values(hash)
|
|
28
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
|
+
expect(hash[SecureHeaders::ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to be_nil
|
|
29
30
|
expect(hash[SecureHeaders::XFrameOptions::HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::DEFAULT_VALUE)
|
|
30
31
|
expect(hash[SecureHeaders::XDownloadOptions::HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::DEFAULT_VALUE)
|
|
31
32
|
expect(hash[SecureHeaders::StrictTransportSecurity::HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::DEFAULT_VALUE)
|
|
@@ -34,18 +35,21 @@ def expect_default_values(hash)
|
|
|
34
35
|
expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE)
|
|
35
36
|
expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to be_nil
|
|
36
37
|
expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil
|
|
38
|
+
expect(hash[SecureHeaders::ClearSiteData::HEADER_NAME]).to be_nil
|
|
39
|
+
expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil
|
|
40
|
+
expect(hash[SecureHeaders::PublicKeyPins::HEADER_NAME]).to be_nil
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
module SecureHeaders
|
|
40
44
|
class Configuration
|
|
41
45
|
class << self
|
|
42
|
-
def
|
|
43
|
-
|
|
46
|
+
def clear_default_config
|
|
47
|
+
remove_instance_variable(:@default_config) if defined?(@default_config)
|
|
44
48
|
end
|
|
45
49
|
end
|
|
46
50
|
end
|
|
47
51
|
end
|
|
48
52
|
|
|
49
53
|
def reset_config
|
|
50
|
-
SecureHeaders::Configuration.
|
|
54
|
+
SecureHeaders::Configuration.clear_default_config
|
|
51
55
|
end
|