secure_headers 3.4.0 → 3.5.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/.ruby-version +1 -1
- data/CHANGELOG.md +66 -0
- data/Gemfile +1 -0
- data/README.md +89 -9
- data/lib/secure_headers/configuration.rb +68 -36
- data/lib/secure_headers/headers/content_security_policy.rb +62 -36
- data/lib/secure_headers/headers/content_security_policy_config.rb +128 -0
- data/lib/secure_headers/headers/policy_management.rb +48 -30
- data/lib/secure_headers/view_helper.rb +8 -0
- data/lib/secure_headers.rb +131 -55
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/configuration_spec.rb +5 -5
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +2 -2
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +25 -34
- data/spec/lib/secure_headers/middleware_spec.rb +13 -1
- data/spec/lib/secure_headers/view_helpers_spec.rb +19 -6
- data/spec/lib/secure_headers_spec.rb +281 -40
- data/spec/spec_helper.rb +3 -2
- metadata +3 -2
|
@@ -40,70 +40,75 @@ module SecureHeaders
|
|
|
40
40
|
report_uri: %w(https://example.com/uri-directive)
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(config))
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
it "requires a :default_src value" do
|
|
47
47
|
expect do
|
|
48
|
-
|
|
48
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(script_src: %('self')))
|
|
49
49
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
it "requires :report_only to be a truthy value" do
|
|
53
53
|
expect do
|
|
54
|
-
|
|
54
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_only: "steve")))
|
|
55
55
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
it "requires :preserve_schemes to be a truthy value" do
|
|
59
59
|
expect do
|
|
60
|
-
|
|
60
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(preserve_schemes: "steve")))
|
|
61
61
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
it "requires :block_all_mixed_content to be a boolean value" do
|
|
65
65
|
expect do
|
|
66
|
-
|
|
66
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(block_all_mixed_content: "steve")))
|
|
67
67
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
it "requires :upgrade_insecure_requests to be a boolean value" do
|
|
71
71
|
expect do
|
|
72
|
-
|
|
72
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(upgrade_insecure_requests: "steve")))
|
|
73
73
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
it "requires all source lists to be an array of strings" do
|
|
77
77
|
expect do
|
|
78
|
-
|
|
78
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: "steve"))
|
|
79
79
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
it "allows nil values" do
|
|
83
83
|
expect do
|
|
84
|
-
|
|
84
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), script_src: ["https:", nil]))
|
|
85
85
|
end.to_not raise_error
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
it "rejects unknown directives / config" do
|
|
89
89
|
expect do
|
|
90
|
-
|
|
90
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), default_src_totally_mispelled: "steve"))
|
|
91
91
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
# this is mostly to ensure people don't use the antiquated shorthands common in other configs
|
|
95
95
|
it "performs light validation on source lists" do
|
|
96
96
|
expect do
|
|
97
|
-
|
|
97
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w(self none inline eval)))
|
|
98
98
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
99
99
|
end
|
|
100
100
|
end
|
|
101
101
|
|
|
102
102
|
describe "#combine_policies" do
|
|
103
103
|
it "combines the default-src value with the override if the directive was unconfigured" do
|
|
104
|
-
|
|
104
|
+
Configuration.default do |config|
|
|
105
|
+
config.csp = {
|
|
106
|
+
default_src: %w(https:)
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, script_src: %w(anothercdn.com))
|
|
105
110
|
csp = ContentSecurityPolicy.new(combined_config)
|
|
106
|
-
expect(csp.name).to eq(
|
|
111
|
+
expect(csp.name).to eq(ContentSecurityPolicyConfig::HEADER_NAME)
|
|
107
112
|
expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com")
|
|
108
113
|
end
|
|
109
114
|
|
|
@@ -115,7 +120,7 @@ module SecureHeaders
|
|
|
115
120
|
}.freeze
|
|
116
121
|
end
|
|
117
122
|
report_uri = "https://report-uri.io/asdf"
|
|
118
|
-
combined_config =
|
|
123
|
+
combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, report_uri: [report_uri])
|
|
119
124
|
csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
|
|
120
125
|
expect(csp.value).to include("report-uri #{report_uri}")
|
|
121
126
|
end
|
|
@@ -127,12 +132,12 @@ module SecureHeaders
|
|
|
127
132
|
report_only: false
|
|
128
133
|
}.freeze
|
|
129
134
|
end
|
|
130
|
-
non_default_source_additions =
|
|
135
|
+
non_default_source_additions = ContentSecurityPolicy::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash|
|
|
131
136
|
hash[directive] = %w("http://example.org)
|
|
132
137
|
end
|
|
133
|
-
combined_config =
|
|
138
|
+
combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, non_default_source_additions)
|
|
134
139
|
|
|
135
|
-
|
|
140
|
+
ContentSecurityPolicy::NON_FETCH_SOURCES.each do |directive|
|
|
136
141
|
expect(combined_config[directive]).to eq(%w("http://example.org))
|
|
137
142
|
end
|
|
138
143
|
|
|
@@ -146,9 +151,9 @@ module SecureHeaders
|
|
|
146
151
|
report_only: false
|
|
147
152
|
}
|
|
148
153
|
end
|
|
149
|
-
combined_config =
|
|
154
|
+
combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, report_only: true)
|
|
150
155
|
csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
|
|
151
|
-
expect(csp.name).to eq(
|
|
156
|
+
expect(csp.name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME)
|
|
152
157
|
end
|
|
153
158
|
|
|
154
159
|
it "overrides the :block_all_mixed_content flag" do
|
|
@@ -158,7 +163,7 @@ module SecureHeaders
|
|
|
158
163
|
block_all_mixed_content: false
|
|
159
164
|
}
|
|
160
165
|
end
|
|
161
|
-
combined_config =
|
|
166
|
+
combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, block_all_mixed_content: true)
|
|
162
167
|
csp = ContentSecurityPolicy.new(combined_config)
|
|
163
168
|
expect(csp.value).to eq("default-src https:; block-all-mixed-content")
|
|
164
169
|
end
|
|
@@ -168,23 +173,9 @@ module SecureHeaders
|
|
|
168
173
|
config.csp = OPT_OUT
|
|
169
174
|
end
|
|
170
175
|
expect do
|
|
171
|
-
|
|
176
|
+
ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, script_src: %w(anothercdn.com))
|
|
172
177
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
173
178
|
end
|
|
174
179
|
end
|
|
175
|
-
|
|
176
|
-
describe "#idempotent_additions?" do
|
|
177
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?(OPT_OUT, script_src: %w(b.com))).to be false }
|
|
178
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(c.com))).to be false }
|
|
179
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: %w(b.com))).to be false }
|
|
180
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(a.com b.com c.com))).to be false }
|
|
181
|
-
|
|
182
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com))).to be true }
|
|
183
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com a.com))).to be true }
|
|
184
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w())).to be true }
|
|
185
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: [nil])).to be true }
|
|
186
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: [nil])).to be true }
|
|
187
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: nil)).to be true }
|
|
188
|
-
end
|
|
189
180
|
end
|
|
190
181
|
end
|
|
@@ -51,7 +51,7 @@ module SecureHeaders
|
|
|
51
51
|
SecureHeaders.use_secure_headers_override(request, "my_custom_config")
|
|
52
52
|
expect(request.env[SECURE_HEADERS_CONFIG]).to be(Configuration.get("my_custom_config"))
|
|
53
53
|
_, env = middleware.call request.env
|
|
54
|
-
expect(env[
|
|
54
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match("example.org")
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
context "secure_cookies" do
|
|
@@ -105,6 +105,18 @@ module SecureHeaders
|
|
|
105
105
|
_, env = cookie_middleware.call request.env
|
|
106
106
|
expect(env['Set-Cookie']).to eq("foo=bar")
|
|
107
107
|
end
|
|
108
|
+
|
|
109
|
+
it "sets the secure cookie flag correctly on interleaved http/https requests" do
|
|
110
|
+
Configuration.default { |config| config.cookies = { secure: true } }
|
|
111
|
+
|
|
112
|
+
request = Rack::Request.new("HTTPS" => "off")
|
|
113
|
+
_, env = cookie_middleware.call request.env
|
|
114
|
+
expect(env['Set-Cookie']).to eq("foo=bar")
|
|
115
|
+
|
|
116
|
+
request = Rack::Request.new("HTTPS" => "on")
|
|
117
|
+
_, env = cookie_middleware.call request.env
|
|
118
|
+
expect(env['Set-Cookie']).to eq("foo=bar; secure")
|
|
119
|
+
end
|
|
108
120
|
end
|
|
109
121
|
end
|
|
110
122
|
end
|
|
@@ -27,6 +27,16 @@ class Message < ERB
|
|
|
27
27
|
background-color: black;
|
|
28
28
|
}
|
|
29
29
|
<% end %>
|
|
30
|
+
|
|
31
|
+
<script nonce="<%= content_security_policy_script_nonce %>">
|
|
32
|
+
alert(1)
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<style nonce="<%= content_security_policy_style_nonce %>">
|
|
36
|
+
body {
|
|
37
|
+
background-color: black;
|
|
38
|
+
}
|
|
39
|
+
</style>
|
|
30
40
|
<%= @name %>
|
|
31
41
|
|
|
32
42
|
TEMPLATE
|
|
@@ -72,8 +82,11 @@ module SecureHeaders
|
|
|
72
82
|
|
|
73
83
|
before(:all) do
|
|
74
84
|
Configuration.default do |config|
|
|
75
|
-
config.csp
|
|
76
|
-
|
|
85
|
+
config.csp = {
|
|
86
|
+
:default_src => %w('self'),
|
|
87
|
+
:script_src => %w('self'),
|
|
88
|
+
:style_src => %w('self')
|
|
89
|
+
}
|
|
77
90
|
end
|
|
78
91
|
end
|
|
79
92
|
|
|
@@ -115,10 +128,10 @@ module SecureHeaders
|
|
|
115
128
|
Message.new(request).result
|
|
116
129
|
_, env = middleware.call request.env
|
|
117
130
|
|
|
118
|
-
expect(env[
|
|
119
|
-
expect(env[
|
|
120
|
-
expect(env[
|
|
121
|
-
expect(env[
|
|
131
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'#{Regexp.escape(expected_hash)}'/)
|
|
132
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/)
|
|
133
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/)
|
|
134
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/)
|
|
122
135
|
end
|
|
123
136
|
end
|
|
124
137
|
end
|
|
@@ -16,7 +16,7 @@ module SecureHeaders
|
|
|
16
16
|
|
|
17
17
|
it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do
|
|
18
18
|
expect do
|
|
19
|
-
SecureHeaders.opt_out_of_header(request,
|
|
19
|
+
SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY)
|
|
20
20
|
end.to raise_error(Configuration::NotYetConfiguredError)
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -29,8 +29,12 @@ module SecureHeaders
|
|
|
29
29
|
|
|
30
30
|
describe "#header_hash_for" do
|
|
31
31
|
it "allows you to opt out of individual headers via API" do
|
|
32
|
-
Configuration.default
|
|
33
|
-
|
|
32
|
+
Configuration.default do |config|
|
|
33
|
+
config.csp = { default_src: %w('self')}
|
|
34
|
+
config.csp_report_only = config.csp
|
|
35
|
+
end
|
|
36
|
+
SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY)
|
|
37
|
+
SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY)
|
|
34
38
|
SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY)
|
|
35
39
|
hash = SecureHeaders.header_hash_for(request)
|
|
36
40
|
expect(hash['Content-Security-Policy-Report-Only']).to be_nil
|
|
@@ -56,7 +60,21 @@ module SecureHeaders
|
|
|
56
60
|
end
|
|
57
61
|
|
|
58
62
|
it "allows you to opt out entirely" do
|
|
59
|
-
|
|
63
|
+
# configure the disabled-by-default headers to ensure they also do not get set
|
|
64
|
+
Configuration.default do |config|
|
|
65
|
+
config.csp = { :default_src => ["example.com"] }
|
|
66
|
+
config.csp_report_only = config.csp
|
|
67
|
+
config.hpkp = {
|
|
68
|
+
report_only: false,
|
|
69
|
+
max_age: 10000000,
|
|
70
|
+
include_subdomains: true,
|
|
71
|
+
report_uri: "https://report-uri.io/example-hpkp",
|
|
72
|
+
pins: [
|
|
73
|
+
{sha256: "abc"},
|
|
74
|
+
{sha256: "123"}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
end
|
|
60
78
|
SecureHeaders.opt_out_of_all_protection(request)
|
|
61
79
|
hash = SecureHeaders.header_hash_for(request)
|
|
62
80
|
ALL_HEADER_CLASSES.each do |klass|
|
|
@@ -82,7 +100,7 @@ module SecureHeaders
|
|
|
82
100
|
SecureHeaders.override_content_security_policy_directives(request, default_src: %w(https:), script_src: %w('self'))
|
|
83
101
|
|
|
84
102
|
hash = SecureHeaders.header_hash_for(request)
|
|
85
|
-
expect(hash[
|
|
103
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; script-src 'self'")
|
|
86
104
|
expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN)
|
|
87
105
|
end
|
|
88
106
|
|
|
@@ -96,14 +114,14 @@ module SecureHeaders
|
|
|
96
114
|
firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox]))
|
|
97
115
|
|
|
98
116
|
# append an unsupported directive
|
|
99
|
-
SecureHeaders.override_content_security_policy_directives(firefox_request, plugin_types: %w(flash))
|
|
117
|
+
SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(flash)})
|
|
100
118
|
# append a supported directive
|
|
101
|
-
SecureHeaders.override_content_security_policy_directives(firefox_request, script_src: %w('self'))
|
|
119
|
+
SecureHeaders.override_content_security_policy_directives(firefox_request, {script_src: %w('self')})
|
|
102
120
|
|
|
103
121
|
hash = SecureHeaders.header_hash_for(firefox_request)
|
|
104
122
|
|
|
105
123
|
# child-src is translated to frame-src
|
|
106
|
-
expect(hash[
|
|
124
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
|
|
107
125
|
end
|
|
108
126
|
|
|
109
127
|
it "produces a hash of headers with default config" do
|
|
@@ -138,6 +156,10 @@ module SecureHeaders
|
|
|
138
156
|
end
|
|
139
157
|
|
|
140
158
|
context "content security policy" do
|
|
159
|
+
let(:chrome_request) {
|
|
160
|
+
Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
|
|
161
|
+
}
|
|
162
|
+
|
|
141
163
|
it "appends a value to csp directive" do
|
|
142
164
|
Configuration.default do |config|
|
|
143
165
|
config.csp = {
|
|
@@ -148,48 +170,53 @@ module SecureHeaders
|
|
|
148
170
|
|
|
149
171
|
SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
|
|
150
172
|
hash = SecureHeaders.header_hash_for(request)
|
|
151
|
-
expect(hash[
|
|
173
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
|
|
152
174
|
end
|
|
153
175
|
|
|
154
|
-
it "
|
|
176
|
+
it "supports named appends" do
|
|
155
177
|
Configuration.default do |config|
|
|
156
178
|
config.csp = {
|
|
157
179
|
default_src: %w('self')
|
|
158
180
|
}
|
|
159
181
|
end
|
|
160
182
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
expect(request.env[SECURE_HEADERS_CONFIG]).to be_nil
|
|
183
|
+
Configuration.named_append(:moar_default_sources) do |request|
|
|
184
|
+
{ default_src: %w(https:)}
|
|
185
|
+
end
|
|
165
186
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
187
|
+
Configuration.named_append(:how_about_a_script_src_too) do |request|
|
|
188
|
+
{ script_src: %w('unsafe-inline')}
|
|
189
|
+
end
|
|
169
190
|
|
|
170
|
-
SecureHeaders.
|
|
171
|
-
|
|
172
|
-
|
|
191
|
+
SecureHeaders.use_content_security_policy_named_append(request, :moar_default_sources)
|
|
192
|
+
SecureHeaders.use_content_security_policy_named_append(request, :how_about_a_script_src_too)
|
|
193
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
173
194
|
|
|
174
|
-
|
|
195
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; script-src 'self' https: 'unsafe-inline'")
|
|
175
196
|
end
|
|
176
197
|
|
|
177
|
-
it "
|
|
178
|
-
|
|
179
|
-
|
|
198
|
+
it "appends a nonce to a missing script-src value" do
|
|
199
|
+
Configuration.default do |config|
|
|
200
|
+
config.csp = {
|
|
201
|
+
default_src: %w('self')
|
|
202
|
+
}
|
|
203
|
+
end
|
|
180
204
|
|
|
181
|
-
#
|
|
182
|
-
|
|
205
|
+
SecureHeaders.content_security_policy_script_nonce(request) # should add the value to the header
|
|
206
|
+
hash = SecureHeaders.header_hash_for(chrome_request)
|
|
207
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match /\Adefault-src 'self'; script-src 'self' 'nonce-.*'\z/
|
|
208
|
+
end
|
|
183
209
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
210
|
+
it "appends a hash to a missing script-src value" do
|
|
211
|
+
Configuration.default do |config|
|
|
212
|
+
config.csp = {
|
|
213
|
+
default_src: %w('self')
|
|
214
|
+
}
|
|
215
|
+
end
|
|
187
216
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
end
|
|
192
|
-
end.to raise_error(Configuration::IllegalPolicyModificationError)
|
|
217
|
+
SecureHeaders.append_content_security_policy_directives(request, script_src: %w('sha256-abc123'))
|
|
218
|
+
hash = SecureHeaders.header_hash_for(chrome_request)
|
|
219
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match /\Adefault-src 'self'; script-src 'self' 'sha256-abc123'\z/
|
|
193
220
|
end
|
|
194
221
|
|
|
195
222
|
it "overrides individual directives" do
|
|
@@ -200,14 +227,19 @@ module SecureHeaders
|
|
|
200
227
|
end
|
|
201
228
|
SecureHeaders.override_content_security_policy_directives(request, default_src: %w('none'))
|
|
202
229
|
hash = SecureHeaders.header_hash_for(request)
|
|
203
|
-
expect(hash[
|
|
230
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'none'")
|
|
204
231
|
end
|
|
205
232
|
|
|
206
233
|
it "overrides non-existant directives" do
|
|
207
|
-
Configuration.default
|
|
234
|
+
Configuration.default do |config|
|
|
235
|
+
config.csp = {
|
|
236
|
+
default_src: %w(https:)
|
|
237
|
+
}
|
|
238
|
+
end
|
|
208
239
|
SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL])
|
|
209
240
|
hash = SecureHeaders.header_hash_for(request)
|
|
210
|
-
expect(hash[
|
|
241
|
+
expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to be_nil
|
|
242
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; img-src data:")
|
|
211
243
|
end
|
|
212
244
|
|
|
213
245
|
it "does not append a nonce when the browser does not support it" do
|
|
@@ -222,7 +254,7 @@ module SecureHeaders
|
|
|
222
254
|
safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5]))
|
|
223
255
|
nonce = SecureHeaders.content_security_policy_script_nonce(safari_request)
|
|
224
256
|
hash = SecureHeaders.header_hash_for(safari_request)
|
|
225
|
-
expect(hash[
|
|
257
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
|
|
226
258
|
end
|
|
227
259
|
|
|
228
260
|
it "appends a nonce to the script-src when used" do
|
|
@@ -234,7 +266,6 @@ module SecureHeaders
|
|
|
234
266
|
}
|
|
235
267
|
end
|
|
236
268
|
|
|
237
|
-
chrome_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
|
|
238
269
|
nonce = SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
239
270
|
|
|
240
271
|
# simulate the nonce being used multiple times in a request:
|
|
@@ -245,6 +276,216 @@ module SecureHeaders
|
|
|
245
276
|
hash = SecureHeaders.header_hash_for(chrome_request)
|
|
246
277
|
expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'")
|
|
247
278
|
end
|
|
279
|
+
|
|
280
|
+
it "uses a nonce for safari 10+" do
|
|
281
|
+
Configuration.default do |config|
|
|
282
|
+
config.csp = {
|
|
283
|
+
default_src: %w('self'),
|
|
284
|
+
script_src: %w(mycdn.com)
|
|
285
|
+
}
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari10]))
|
|
289
|
+
nonce = SecureHeaders.content_security_policy_script_nonce(safari_request)
|
|
290
|
+
hash = SecureHeaders.header_hash_for(safari_request)
|
|
291
|
+
expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'")
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
it "supports the deprecated `report_only: true` format" do
|
|
295
|
+
expect(Kernel).to receive(:warn).once
|
|
296
|
+
|
|
297
|
+
Configuration.default do |config|
|
|
298
|
+
config.csp = {
|
|
299
|
+
default_src: %w('self'),
|
|
300
|
+
report_only: true
|
|
301
|
+
}
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
expect(Configuration.get.csp).to eq(OPT_OUT)
|
|
305
|
+
expect(Configuration.get.csp_report_only).to be_a(ContentSecurityPolicyReportOnlyConfig)
|
|
306
|
+
|
|
307
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
308
|
+
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to be_nil
|
|
309
|
+
expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to eq("default-src 'self'")
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
it "Raises an error if csp_report_only is used with `report_only: false`" do
|
|
313
|
+
expect do
|
|
314
|
+
Configuration.default do |config|
|
|
315
|
+
config.csp_report_only = {
|
|
316
|
+
default_src: %w('self'),
|
|
317
|
+
report_only: false
|
|
318
|
+
}
|
|
319
|
+
end
|
|
320
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
context "setting two headers" do
|
|
324
|
+
before(:each) do
|
|
325
|
+
Configuration.default do |config|
|
|
326
|
+
config.csp = {
|
|
327
|
+
default_src: %w('self')
|
|
328
|
+
}
|
|
329
|
+
config.csp_report_only = config.csp
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
it "sets identical values when the configs are the same" do
|
|
334
|
+
Configuration.default do |config|
|
|
335
|
+
config.csp = {
|
|
336
|
+
default_src: %w('self')
|
|
337
|
+
}
|
|
338
|
+
config.csp_report_only = {
|
|
339
|
+
default_src: %w('self')
|
|
340
|
+
}
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
344
|
+
expect(hash['Content-Security-Policy']).to eq("default-src 'self'")
|
|
345
|
+
expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
it "sets different headers when the configs are different" do
|
|
349
|
+
Configuration.default do |config|
|
|
350
|
+
config.csp = {
|
|
351
|
+
default_src: %w('self')
|
|
352
|
+
}
|
|
353
|
+
config.csp_report_only = config.csp.merge({script_src: %w('self')})
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
357
|
+
expect(hash['Content-Security-Policy']).to eq("default-src 'self'")
|
|
358
|
+
expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self'")
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
it "allows you to opt-out of enforced CSP" do
|
|
362
|
+
Configuration.default do |config|
|
|
363
|
+
config.csp = SecureHeaders::OPT_OUT
|
|
364
|
+
config.csp_report_only = {
|
|
365
|
+
default_src: %w('self')
|
|
366
|
+
}
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
370
|
+
expect(hash['Content-Security-Policy']).to be_nil
|
|
371
|
+
expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
it "opts-out of enforced CSP when only csp_report_only is set" do
|
|
375
|
+
expect(Kernel).to receive(:warn).once
|
|
376
|
+
Configuration.default do |config|
|
|
377
|
+
config.csp_report_only = {
|
|
378
|
+
default_src: %w('self')
|
|
379
|
+
}
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
383
|
+
expect(hash['Content-Security-Policy']).to be_nil
|
|
384
|
+
expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
it "allows you to set csp_report_only before csp" do
|
|
388
|
+
expect(Kernel).to receive(:warn).once
|
|
389
|
+
Configuration.default do |config|
|
|
390
|
+
config.csp_report_only = {
|
|
391
|
+
default_src: %w('self')
|
|
392
|
+
}
|
|
393
|
+
config.csp = config.csp_report_only.merge({script_src: %w('unsafe-inline')})
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
397
|
+
expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' 'unsafe-inline'")
|
|
398
|
+
expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
it "allows appending to the enforced policy" do
|
|
402
|
+
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced)
|
|
403
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
404
|
+
expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com")
|
|
405
|
+
expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
it "allows appending to the report only policy" do
|
|
409
|
+
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only)
|
|
410
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
411
|
+
expect(hash['Content-Security-Policy']).to eq("default-src 'self'")
|
|
412
|
+
expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com")
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
it "allows appending to both policies" do
|
|
416
|
+
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both)
|
|
417
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
418
|
+
expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com")
|
|
419
|
+
expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com")
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
it "allows overriding the enforced policy" do
|
|
423
|
+
SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced)
|
|
424
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
425
|
+
expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src anothercdn.com")
|
|
426
|
+
expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
it "allows overriding the report only policy" do
|
|
430
|
+
SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only)
|
|
431
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
432
|
+
expect(hash['Content-Security-Policy']).to eq("default-src 'self'")
|
|
433
|
+
expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src anothercdn.com")
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
it "allows overriding both policies" do
|
|
437
|
+
SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both)
|
|
438
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
439
|
+
expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src anothercdn.com")
|
|
440
|
+
expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src anothercdn.com")
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
context "when inferring which config to modify" do
|
|
444
|
+
it "updates the enforced header when configured" do
|
|
445
|
+
Configuration.default do |config|
|
|
446
|
+
config.csp = {
|
|
447
|
+
default_src: %w('self')
|
|
448
|
+
}
|
|
449
|
+
end
|
|
450
|
+
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
|
|
451
|
+
|
|
452
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
453
|
+
expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com")
|
|
454
|
+
expect(hash['Content-Security-Policy-Report-Only']).to be_nil
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
it "updates the report only header when configured" do
|
|
458
|
+
Configuration.default do |config|
|
|
459
|
+
config.csp = OPT_OUT
|
|
460
|
+
config.csp_report_only = {
|
|
461
|
+
default_src: %w('self')
|
|
462
|
+
}
|
|
463
|
+
end
|
|
464
|
+
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
|
|
465
|
+
|
|
466
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
467
|
+
expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com")
|
|
468
|
+
expect(hash['Content-Security-Policy']).to be_nil
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
it "updates both headers if both are configured" do
|
|
472
|
+
Configuration.default do |config|
|
|
473
|
+
config.csp = {
|
|
474
|
+
default_src: %w(enforced.com)
|
|
475
|
+
}
|
|
476
|
+
config.csp_report_only = {
|
|
477
|
+
default_src: %w(reportonly.com)
|
|
478
|
+
}
|
|
479
|
+
end
|
|
480
|
+
SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
|
|
481
|
+
|
|
482
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
483
|
+
expect(hash['Content-Security-Policy']).to eq("default-src enforced.com; script-src enforced.com anothercdn.com")
|
|
484
|
+
expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src reportonly.com; script-src reportonly.com anothercdn.com")
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
end
|
|
488
|
+
end
|
|
248
489
|
end
|
|
249
490
|
end
|
|
250
491
|
|
|
@@ -260,7 +501,7 @@ module SecureHeaders
|
|
|
260
501
|
it "validates your csp config upon configuration" do
|
|
261
502
|
expect do
|
|
262
503
|
Configuration.default do |config|
|
|
263
|
-
config.csp = {
|
|
504
|
+
config.csp = { ContentSecurityPolicy::DEFAULT_SRC => '123456' }
|
|
264
505
|
end
|
|
265
506
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
266
507
|
end
|