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.
@@ -40,70 +40,75 @@ module SecureHeaders
40
40
  report_uri: %w(https://example.com/uri-directive)
41
41
  }
42
42
 
43
- CSP.validate_config!(config)
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
- CSP.validate_config!(script_src: %('self'))
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
- CSP.validate_config!(default_opts.merge(report_only: "steve"))
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
- CSP.validate_config!(default_opts.merge(preserve_schemes: "steve"))
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
- CSP.validate_config!(default_opts.merge(block_all_mixed_content: "steve"))
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
- CSP.validate_config!(default_opts.merge(upgrade_insecure_requests: "steve"))
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
- CSP.validate_config!(default_src: "steve")
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
- CSP.validate_config!(default_src: %w('self'), script_src: ["https:", nil])
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
- CSP.validate_config!(default_src: %w('self'), default_src_totally_mispelled: "steve")
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
- CSP.validate_config!(default_src: %w(self none inline eval))
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
- combined_config = CSP.combine_policies(Configuration.default.csp, script_src: %w(anothercdn.com))
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(CSP::HEADER_NAME)
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 = CSP.combine_policies(Configuration.get.csp, report_uri: [report_uri])
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 = CSP::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash|
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 = CSP.combine_policies(Configuration.get.csp, non_default_source_additions)
138
+ combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, non_default_source_additions)
134
139
 
135
- CSP::NON_FETCH_SOURCES.each do |directive|
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 = CSP.combine_policies(Configuration.get.csp, report_only: true)
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(CSP::REPORT_ONLY)
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 = CSP.combine_policies(Configuration.get.csp, block_all_mixed_content: true)
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
- CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com))
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[CSP::HEADER_NAME]).to match("example.org")
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[:script_src] = %w('self')
76
- config.csp[:style_src] = %w('self')
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[CSP::HEADER_NAME]).to match(/script-src[^;]*'#{Regexp.escape(expected_hash)}'/)
119
- expect(env[CSP::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/)
120
- expect(env[CSP::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/)
121
- expect(env[CSP::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/)
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, CSP::CONFIG_KEY)
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
- SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY)
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
- Configuration.default
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[CSP::HEADER_NAME]).to eq("default-src https:; script-src 'self'")
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[CSP::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
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[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
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 "dups global configuration just once when overriding n times and only calls idempotent_additions? once" do
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
- expect(CSP).to receive(:idempotent_additions?).once
162
-
163
- # before an override occurs, the env is empty
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
- SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
167
- new_config = SecureHeaders.config_for(request)
168
- expect(new_config).to_not be(Configuration.get)
187
+ Configuration.named_append(:how_about_a_script_src_too) do |request|
188
+ { script_src: %w('unsafe-inline')}
189
+ end
169
190
 
170
- SecureHeaders.override_content_security_policy_directives(request, script_src: %w(yet.anothercdn.com))
171
- current_config = SecureHeaders.config_for(request)
172
- expect(current_config).to be(new_config)
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
- SecureHeaders.header_hash_for(request)
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 "doesn't allow you to muck with csp configs when a dynamic policy is in use" do
178
- default_config = Configuration.default
179
- expect { default_config.csp = {} }.to raise_error(NoMethodError)
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
- # config is frozen
182
- expect { default_config.send(:csp=, {}) }.to raise_error(RuntimeError)
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
- SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
185
- new_config = SecureHeaders.config_for(request)
186
- expect { new_config.send(:csp=, {}) }.to raise_error(Configuration::IllegalPolicyModificationError)
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
- expect do
189
- new_config.instance_eval do
190
- new_config.csp = {}
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[CSP::HEADER_NAME]).to eq("default-src 'none'")
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[CSP::HEADER_NAME]).to eq("default-src https:; img-src data:")
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[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
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 = { CSP::DEFAULT_SRC => '123456' }
504
+ config.csp = { ContentSecurityPolicy::DEFAULT_SRC => '123456' }
264
505
  end
265
506
  end.to raise_error(ContentSecurityPolicyConfigError)
266
507
  end