secure_headers 1.4.1 → 2.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of secure_headers might be problematic. Click here for more details.

Files changed (32) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -8
  3. data/Gemfile +2 -2
  4. data/Guardfile +8 -0
  5. data/README.md +102 -48
  6. data/Rakefile +0 -116
  7. data/fixtures/rails_3_2_12/app/views/layouts/application.html.erb +1 -1
  8. data/fixtures/rails_3_2_12/app/views/other_things/index.html.erb +2 -1
  9. data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +1 -1
  10. data/fixtures/rails_3_2_12/config/script_hashes.yml +5 -0
  11. data/fixtures/rails_3_2_12/config.ru +3 -0
  12. data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +50 -18
  13. data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +1 -1
  14. data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +1 -2
  15. data/lib/secure_headers/hash_helper.rb +7 -0
  16. data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +22 -0
  17. data/lib/secure_headers/headers/content_security_policy.rb +141 -137
  18. data/lib/secure_headers/railtie.rb +0 -22
  19. data/lib/secure_headers/version.rb +1 -1
  20. data/lib/secure_headers/view_helper.rb +68 -0
  21. data/lib/secure_headers.rb +51 -17
  22. data/lib/tasks/tasks.rake +48 -0
  23. data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +47 -0
  24. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +83 -208
  25. data/spec/lib/secure_headers_spec.rb +16 -62
  26. data/spec/spec_helper.rb +25 -1
  27. metadata +22 -24
  28. data/HISTORY.md +0 -162
  29. data/app/controllers/content_security_policy_controller.rb +0 -76
  30. data/config/curl-ca-bundle.crt +0 -5420
  31. data/config/routes.rb +0 -3
  32. data/spec/controllers/content_security_policy_controller_spec.rb +0 -90
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+ require 'secure_headers/headers/content_security_policy/script_hash_middleware'
3
+
4
+ module SecureHeaders
5
+ describe ContentSecurityPolicy::ScriptHashMiddleware do
6
+
7
+ let(:app) { double(:call => [200, headers, '']) }
8
+ let(:env) { double }
9
+ let(:headers) { double }
10
+
11
+ let(:default_config) do
12
+ {
13
+ :disable_fill_missing => true,
14
+ :default_src => 'https://*',
15
+ :report_uri => '/csp_report',
16
+ :script_src => 'inline eval https://* data:',
17
+ :style_src => "inline https://* about:"
18
+ }
19
+ end
20
+
21
+ def should_assign_header name, value
22
+ expect(headers).to receive(:[]=).with(name, value)
23
+ end
24
+
25
+ def call_middleware(hashes = [])
26
+ options = {
27
+ :ua => USER_AGENTS[:chrome]
28
+ }
29
+ expect(env).to receive(:[]).with(HASHES_ENV_KEY).and_return(hashes)
30
+ expect(env).to receive(:[]).with(ENV_KEY).and_return(
31
+ :config => default_config,
32
+ :options => options
33
+ )
34
+ ContentSecurityPolicy::ScriptHashMiddleware.new(app).call(env)
35
+ end
36
+
37
+ it "adds hashes stored in env to the header" do
38
+ should_assign_header(HEADER_NAME + "-Report-Only", /script-src[^;]*'sha256-/)
39
+ call_middleware(['sha256-abc123'])
40
+ end
41
+
42
+ it "leaves things alone when no hashes are saved to env" do
43
+ should_assign_header(HEADER_NAME + "-Report-Only", /script-src[^;]*(?!'sha256-)/)
44
+ call_middleware()
45
+ end
46
+ end
47
+ end
@@ -4,12 +4,11 @@ module SecureHeaders
4
4
  describe ContentSecurityPolicy do
5
5
  let(:default_opts) do
6
6
  {
7
- :disable_chrome_extension => true,
8
7
  :disable_fill_missing => true,
9
- :default_src => 'https://*',
8
+ :default_src => 'https:',
10
9
  :report_uri => '/csp_report',
11
- :script_src => 'inline eval https://* data:',
12
- :style_src => "inline https://* about:"
10
+ :script_src => 'inline eval https: data:',
11
+ :style_src => "inline https: about:"
13
12
  }
14
13
  end
15
14
 
@@ -30,44 +29,41 @@ module SecureHeaders
30
29
 
31
30
  describe "#name" do
32
31
  context "when supplying options to override request" do
33
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => IE).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
34
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
35
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX_23).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
36
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
37
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME_25).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
32
+ specify { expect(ContentSecurityPolicy.new(default_opts, :ua => IE).name).to eq(HEADER_NAME + "-Report-Only")}
33
+ specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX).name).to eq(HEADER_NAME + "-Report-Only")}
34
+ specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX_23).name).to eq(HEADER_NAME + "-Report-Only")}
35
+ specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME).name).to eq(HEADER_NAME + "-Report-Only")}
36
+ specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME_25).name).to eq(HEADER_NAME + "-Report-Only")}
38
37
  end
39
38
 
40
39
  context "when in report-only mode" do
41
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(IE)).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
42
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX)).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
43
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX_23)).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
44
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME)).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
45
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME_25)).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
40
+ specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(IE)).name).to eq(HEADER_NAME + "-Report-Only")}
41
+ specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX)).name).to eq(HEADER_NAME + "-Report-Only")}
42
+ specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX_23)).name).to eq(HEADER_NAME + "-Report-Only")}
43
+ specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME)).name).to eq(HEADER_NAME + "-Report-Only")}
44
+ specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME_25)).name).to eq(HEADER_NAME + "-Report-Only")}
46
45
  end
47
46
 
48
47
  context "when in enforce mode" do
49
48
  let(:opts) { default_opts.merge(:enforce => true)}
50
49
 
51
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(IE)).name).to eq(STANDARD_HEADER_NAME)}
52
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX)).name).to eq(STANDARD_HEADER_NAME)}
53
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX_23)).name).to eq(STANDARD_HEADER_NAME)}
54
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME)).name).to eq(STANDARD_HEADER_NAME)}
55
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME_25)).name).to eq(STANDARD_HEADER_NAME)}
50
+ specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(IE)).name).to eq(HEADER_NAME)}
51
+ specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX)).name).to eq(HEADER_NAME)}
52
+ specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX_23)).name).to eq(HEADER_NAME)}
53
+ specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME)).name).to eq(HEADER_NAME)}
54
+ specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME_25)).name).to eq(HEADER_NAME)}
56
55
  end
56
+ end
57
57
 
58
- context "when in experimental mode" do
59
- let(:opts) { default_opts.merge(:enforce => true).merge(:experimental => {})}
60
- specify { expect(ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(IE)}).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
61
- specify { expect(ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(FIREFOX)}).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
62
- specify { expect(ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(FIREFOX_23)}).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
63
- specify { expect(ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(CHROME)}).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
64
- specify { expect(ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(CHROME_25)}).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
58
+ context "when using hash sources" do
59
+ it "adds hashes and unsafe-inline to the script-src" do
60
+ policy = ContentSecurityPolicy.new(default_opts.merge(:script_hashes => ['sha256-abc123']))
61
+ expect(policy.value).to match /script-src[^;]*'sha256-abc123'/
65
62
  end
66
63
  end
67
64
 
68
65
  describe "#normalize_csp_options" do
69
66
  before(:each) do
70
- default_opts.delete(:disable_chrome_extension)
71
67
  default_opts.delete(:disable_fill_missing)
72
68
  default_opts[:script_src] << ' self none'
73
69
  @opts = default_opts
@@ -76,11 +72,11 @@ module SecureHeaders
76
72
  context "Content-Security-Policy" do
77
73
  it "converts the script values to their equivilents" do
78
74
  csp = ContentSecurityPolicy.new(@opts, :request => request_for(CHROME))
79
- expect(csp.value).to include("script-src 'unsafe-inline' 'unsafe-eval' https://* data: 'self' 'none'")
75
+ expect(csp.value).to include("script-src 'unsafe-inline' 'unsafe-eval' https: data: 'self' 'none'")
80
76
  end
81
77
 
82
78
  it "adds a @enforce and @app_name variables to the report uri" do
83
- opts = @opts.merge(:tag_report_uri => true, :enforce => true, :app_name => 'twitter')
79
+ opts = @opts.merge(:tag_report_uri => true, :enforce => true, :app_name => lambda { 'twitter' })
84
80
  csp = ContentSecurityPolicy.new(opts, :request => request_for(CHROME))
85
81
  expect(csp.value).to include("/csp_report?enforce=true&app_name=twitter")
86
82
  end
@@ -98,7 +94,7 @@ module SecureHeaders
98
94
  }
99
95
 
100
96
  csp = ContentSecurityPolicy.new(opts)
101
- expect(csp.report_uri).to eq("http://lambda/result")
97
+ expect(csp.value).to match("report-uri http://lambda/result")
102
98
  end
103
99
 
104
100
  it "accepts procs for other fields" do
@@ -115,130 +111,34 @@ module SecureHeaders
115
111
  end
116
112
  end
117
113
 
118
- describe "#same_origin?" do
119
- let(:origin) {"https://example.com:123"}
120
-
121
- it "matches when host, scheme, and port match" do
122
- csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "https://example.com"))
123
- expect(csp.send(:same_origin?)).to be true
124
-
125
- csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "https://example.com:443"))
126
- expect(csp.send(:same_origin?)).to be true
127
-
128
- csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com:123'}, :request => request_for(FIREFOX, "https://example.com:123"))
129
- expect(csp.send(:same_origin?)).to be true
130
-
131
- csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://example.com"))
132
- expect(csp.send(:same_origin?)).to be true
133
-
134
- csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com:80'}, :request => request_for(FIREFOX, "http://example.com"))
135
- expect(csp.send(:same_origin?)).to be true
136
-
137
- csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://example.com:80"))
138
- expect(csp.send(:same_origin?)).to be true
139
- end
140
-
141
- it "does not match port mismatches" do
142
- csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://example.com:81"))
143
- expect(csp.send(:same_origin?)).to be false
144
- end
145
-
146
- it "does not match host mismatches" do
147
- csp = ContentSecurityPolicy.new({:report_uri => 'http://twitter.com'}, :request => request_for(FIREFOX, "http://example.com"))
148
- expect(csp.send(:same_origin?)).to be false
149
- end
150
-
151
- it "does not match host mismatches because of subdomains" do
152
- csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://sub.example.com"))
153
- expect(csp.send(:same_origin?)).to be false
154
- end
155
-
156
- it "does not match scheme mismatches" do
157
- csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "ftp://example.com"))
158
- expect(csp.send(:same_origin?)).to be false
159
- end
160
-
161
- it "does not match on substring collisions" do
162
- csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "https://anotherexample.com"))
163
- expect(csp.send(:same_origin?)).to be false
164
- end
165
- end
166
-
167
- describe "#normalize_reporting_endpoint" do
168
- let(:opts) {{:report_uri => 'https://example.com/csp', :forward_endpoint => anything}}
169
-
170
- context "when using firefox" do
171
- it "updates the report-uri when posting to a different host" do
172
- csp = ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX, "https://anexample.com"))
173
- expect(csp.report_uri).to eq(FF_CSP_ENDPOINT)
174
- end
175
-
176
- it "doesn't change report-uri if a path supplied" do
177
- csp = ContentSecurityPolicy.new({:report_uri => "/csp_reports"}, :request => request_for(FIREFOX, "https://anexample.com"))
178
- expect(csp.report_uri).to eq("/csp_reports")
179
- end
180
-
181
- it "forwards if the request_uri is set to a non-matching value" do
182
- csp = ContentSecurityPolicy.new({:report_uri => "https://another.example.com", :forward_endpoint => '/somewhere'}, :ua => "Firefox", :request_uri => "https://anexample.com")
183
- expect(csp.report_uri).to eq(FF_CSP_ENDPOINT)
184
- end
185
- end
186
-
187
- it "does not update the URI is the report_uri is on the same origin" do
188
- opts = {:report_uri => 'https://example.com/csp', :forward_endpoint => 'https://anotherexample.com'}
189
- csp = ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX, "https://example.com/somewhere"))
190
- expect(csp.report_uri).to eq('https://example.com/csp')
191
- end
192
-
193
- it "does not update the report-uri when using a non-firefox browser" do
194
- csp = ContentSecurityPolicy.new(opts, :request => request_for(CHROME))
195
- expect(csp.report_uri).to eq('https://example.com/csp')
196
- end
197
-
198
- context "when using a protocol-relative value for report-uri" do
199
- let(:opts) {
200
- {
201
- :default_src => 'self',
202
- :report_uri => '//example.com/csp'
203
- }
204
- }
205
-
206
- it "uses the current protocol" do
207
- csp = ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX, '/', :ssl => true))
208
- expect(csp.value).to match(%r{report-uri https://example.com/csp;})
209
-
210
- csp = ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX))
211
- expect(csp.value).to match(%r{report-uri http://example.com/csp;})
212
- end
213
-
214
- it "uses the pre-configured https protocol" do
215
- csp = ContentSecurityPolicy.new(opts, :ua => "Firefox", :ssl => true)
216
- expect(csp.value).to match(%r{report-uri https://example.com/csp;})
217
- end
218
-
219
- it "uses the pre-configured http protocol" do
220
- csp = ContentSecurityPolicy.new(opts, :ua => "Firefox", :ssl => false)
221
- expect(csp.value).to match(%r{report-uri http://example.com/csp;})
222
- end
223
- end
224
- end
225
-
226
114
  describe "#value" do
227
115
  it "raises an exception when default-src is missing" do
228
116
  csp = ContentSecurityPolicy.new({:script_src => 'anything'}, :request => request_for(CHROME))
229
117
  expect {
230
118
  csp.value
231
- }.to raise_error(ContentSecurityPolicyBuildError, "Couldn't build CSP header :( Expected to find default_src directive value")
119
+ }.to raise_error(RuntimeError)
120
+ end
121
+
122
+ context "CSP level 2 directives" do
123
+ let(:config) { {:default_src => 'self'} }
124
+ ::SecureHeaders::ContentSecurityPolicy::Constants::NON_DEFAULT_SOURCES.each do |non_default_source|
125
+ it "supports all level 2 directives" do
126
+ directive_name = ::SecureHeaders::ContentSecurityPolicy.send(:symbol_to_hyphen_case, non_default_source)
127
+ config.merge!({ non_default_source => "value" })
128
+ csp = ContentSecurityPolicy.new(config, :request => request_for(CHROME))
129
+ expect(csp.value).to match(/#{directive_name} value;/)
130
+ end
131
+ end
232
132
  end
233
133
 
234
134
  context "auto-whitelists data: uris for img-src" do
235
135
  it "sets the value if no img-src specified" do
236
- csp = ContentSecurityPolicy.new({:default_src => 'self', :disable_fill_missing => true, :disable_chrome_extension => true}, :request => request_for(CHROME))
136
+ csp = ContentSecurityPolicy.new({:default_src => 'self', :disable_fill_missing => true}, :request => request_for(CHROME))
237
137
  expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
238
138
  end
239
139
 
240
140
  it "appends the value if img-src is specified" do
241
- csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_fill_missing => true, :disable_chrome_extension => true}, :request => request_for(CHROME))
141
+ csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_fill_missing => true}, :request => request_for(CHROME))
242
142
  expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
243
143
  end
244
144
  end
@@ -246,7 +146,7 @@ module SecureHeaders
246
146
  it "fills in directives without values with default-src value" do
247
147
  options = default_opts.merge(:disable_fill_missing => false)
248
148
  csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
249
- value = "default-src https://*; connect-src https://*; font-src https://*; frame-src https://*; img-src https://* data:; media-src https://*; object-src https://*; script-src 'unsafe-inline' 'unsafe-eval' https://* data:; style-src 'unsafe-inline' https://* about:; report-uri /csp_report;"
149
+ value = "default-src https:; connect-src https:; font-src https:; frame-src https:; img-src https: data:; media-src https:; object-src https:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;"
250
150
  expect(csp.value).to eq(value)
251
151
  end
252
152
 
@@ -258,19 +158,14 @@ module SecureHeaders
258
158
  context "Firefox" do
259
159
  it "builds a csp header for firefox" do
260
160
  csp = ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX))
261
- expect(csp.value).to eq("default-src https://*; img-src https://* data:; script-src 'unsafe-inline' 'unsafe-eval' https://* data:; style-src 'unsafe-inline' https://* about:; report-uri /csp_report;")
161
+ expect(csp.value).to eq("default-src https:; img-src https: data:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;")
262
162
  end
263
163
  end
264
164
 
265
165
  context "Chrome" do
266
166
  it "builds a csp header for chrome" do
267
167
  csp = ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME))
268
- expect(csp.value).to eq("default-src https://*; img-src https://* data:; script-src 'unsafe-inline' 'unsafe-eval' https://* data:; style-src 'unsafe-inline' https://* about:; report-uri /csp_report;")
269
- end
270
-
271
- it "ignores :forward_endpoint settings" do
272
- csp = ContentSecurityPolicy.new(@options_with_forwarding, :request => request_for(CHROME))
273
- expect(csp.value).to match(/report-uri #{@options_with_forwarding[:report_uri]};/)
168
+ expect(csp.value).to eq("default-src https:; img-src https: data:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;")
274
169
  end
275
170
  end
276
171
 
@@ -299,41 +194,19 @@ module SecureHeaders
299
194
  end
300
195
  end
301
196
 
302
- context "when supplying a experimental values" do
303
- let(:options) {{
304
- :disable_chrome_extension => true,
305
- :disable_fill_missing => true,
306
- :default_src => 'self',
307
- :script_src => 'https://*',
308
- :experimental => {
309
- :script_src => 'self'
310
- }
311
- }}
312
-
313
- it "returns the original value" do
314
- header = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
315
- expect(header.value).to eq("default-src 'self'; img-src 'self' data:; script-src https://*;")
316
- end
317
-
318
- it "it returns the experimental value if requested" do
319
- header = ContentSecurityPolicy.new(options, {:request => request_for(CHROME), :experimental => true})
320
- expect(header.value).not_to match(/https/)
321
- end
322
- end
323
-
324
197
  context "when supplying additional http directive values" do
325
198
  let(:options) {
326
199
  default_opts.merge({
327
200
  :http_additions => {
328
- :frame_src => "http://*",
329
- :img_src => "http://*"
201
+ :frame_src => "http:",
202
+ :img_src => "http:"
330
203
  }
331
204
  })
332
205
  }
333
206
 
334
207
  it "adds directive values for headers on http" do
335
208
  csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
336
- expect(csp.value).to eq("default-src https://*; frame-src http://*; img-src http://* data:; script-src 'unsafe-inline' 'unsafe-eval' https://* data:; style-src 'unsafe-inline' https://* about:; report-uri /csp_report;")
209
+ expect(csp.value).to eq("default-src https:; frame-src http:; img-src http: data:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;")
337
210
  end
338
211
 
339
212
  it "does not add the directive values if requesting https" do
@@ -345,45 +218,47 @@ module SecureHeaders
345
218
  csp = ContentSecurityPolicy.new(options, :ua => "Chrome", :ssl => true)
346
219
  expect(csp.value).not_to match(/http:/)
347
220
  end
221
+ end
348
222
 
349
- context "when supplying an experimental block" do
350
- # this simulates the situation where we are enforcing that scripts
351
- # only come from http[s]? depending if we're on ssl or not. The
352
- # report only tag will allow scripts from self over ssl, and
353
- # from a secure CDN over non-ssl
354
- let(:options) {{
355
- :disable_chrome_extension => true,
356
- :disable_fill_missing => true,
357
- :default_src => 'self',
358
- :script_src => 'https://*',
359
- :http_additions => {
360
- :script_src => 'http://*'
361
- },
362
- :experimental => {
363
- :script_src => 'self',
364
- :http_additions => {
365
- :script_src => 'https://mycdn.example.com'
366
- }
367
- }
368
- }}
369
- # for comparison purposes, if not using the experimental header this would produce
370
- # "allow 'self'; script-src https://*" for https requests
371
- # and
372
- # "allow 'self'; script-src https://* http://*" for http requests
373
-
374
- it "uses the value in the experimental block over SSL" do
375
- csp = ContentSecurityPolicy.new(options, :experimental => true, :request => request_for(FIREFOX, '/', :ssl => true))
376
- expect(csp.value).to eq("default-src 'self'; img-src 'self' data:; script-src 'self';")
223
+ describe "class methods" do
224
+ let(:ua) { CHROME }
225
+ let(:env) do
226
+ double.tap do |env|
227
+ allow(env).to receive(:[]).with('HTTP_USER_AGENT').and_return(ua)
377
228
  end
229
+ end
230
+ let(:request) do
231
+ double(
232
+ :ssl? => true,
233
+ :url => 'https://example.com',
234
+ :env => env
235
+ )
236
+ end
237
+
238
+ describe ".add_to_env" do
239
+ let(:controller) { double }
240
+ let(:config) { {:default_src => 'self'} }
241
+ let(:options) { {:controller => controller} }
378
242
 
379
- it "detects the :ssl => true option" do
380
- csp = ContentSecurityPolicy.new(options, :experimental => true, :ua => FIREFOX, :ssl => true)
381
- expect(csp.value).to eq("default-src 'self'; img-src 'self' data:; script-src 'self';")
243
+ it "adds metadata to env" do
244
+ metadata = {
245
+ :config => config,
246
+ :options => options
247
+ }
248
+ expect(ContentSecurityPolicy).to receive(:options_from_request).and_return(options)
249
+ expect(env).to receive(:[]=).with(ContentSecurityPolicy::ENV_KEY, metadata)
250
+ ContentSecurityPolicy.add_to_env(request, controller, config)
382
251
  end
252
+ end
383
253
 
384
- it "merges the values from experimental/http_additions when not over SSL" do
385
- csp = ContentSecurityPolicy.new(options, :experimental => true, :request => request_for(FIREFOX))
386
- expect(csp.value).to eq("default-src 'self'; img-src 'self' data:; script-src 'self' https://mycdn.example.com;")
254
+ describe ".options_from_request" do
255
+ it "extracts options from request" do
256
+ options = ContentSecurityPolicy.options_from_request(request)
257
+ expect(options).to eql({
258
+ :ua => ua,
259
+ :ssl => true,
260
+ :request_uri => 'https://example.com'
261
+ })
387
262
  end
388
263
  end
389
264
  end
@@ -19,25 +19,6 @@ describe SecureHeaders do
19
19
  end
20
20
 
21
21
  ALL_HEADERS = Hash[[:hsts, :csp, :x_frame_options, :x_content_type_options, :x_xss_protection].map{|header| [header, false]}]
22
- USER_AGENTS = {
23
- :firefox => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1',
24
- :chrome => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.56 Safari/536.5',
25
- :ie => 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)',
26
- :opera => 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00',
27
- :ios5 => "Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3",
28
- :ios6 => "Mozilla/5.0 (iPhone; CPU iPhone OS 614 like Mac OS X) AppleWebKit/536.26 (KHTML like Gecko) Version/6.0 Mobile/10B350 Safari/8536.25",
29
- :safari5 => "Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko ) Version/5.1 Mobile/9B176 Safari/7534.48.3",
30
- :safari5_1 => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10",
31
- :safari6 => "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/536.30.1 (KHTML like Gecko) Version/6.0.5 Safari/536.30.1"
32
- }
33
-
34
- def should_assign_header name, value
35
- expect(response.headers).to receive(:[]=).with(name, value)
36
- end
37
-
38
- def should_not_assign_header name
39
- expect(response.headers).not_to receive(:[]=).with(name, anything)
40
- end
41
22
 
42
23
  def stub_user_agent val
43
24
  allow(request).to receive_message_chain(:env, :[]).and_return(val)
@@ -67,14 +48,6 @@ describe SecureHeaders do
67
48
  subject.set_x_download_options_header
68
49
  end
69
50
 
70
- describe "#ensure_security_headers" do
71
- it "sets a before filter" do
72
- options = {}
73
- expect(DummyClass).to receive(:before_filter).exactly(6).times
74
- DummyClass.ensure_security_headers(options)
75
- end
76
- end
77
-
78
51
  describe "#set_header" do
79
52
  it "accepts name/value pairs" do
80
53
  should_assign_header("X-Hipster-Ipsum", "kombucha")
@@ -88,9 +61,6 @@ describe SecureHeaders do
88
61
  end
89
62
 
90
63
  describe "#set_security_headers" do
91
- before(:each) do
92
- allow(SecureHeaders::ContentSecurityPolicy).to receive(:new).and_return(double.as_null_object)
93
- end
94
64
  USER_AGENTS.each do |name, useragent|
95
65
  it "sets all default headers for #{name} (smoke test)" do
96
66
  stub_user_agent(useragent)
@@ -139,8 +109,19 @@ describe SecureHeaders do
139
109
 
140
110
  it "does not set the CSP header if disabled" do
141
111
  stub_user_agent(USER_AGENTS[:chrome])
142
- should_not_assign_header(STANDARD_HEADER_NAME)
143
- subject.set_csp_header(options_for(:csp).merge(:csp => false))
112
+ should_not_assign_header(HEADER_NAME)
113
+ subject.set_csp_header(false)
114
+ end
115
+
116
+ it "saves the options to the env when using script hashes" do
117
+ opts = {
118
+ :default_src => 'self',
119
+ :script_hash_middleware => true
120
+ }
121
+ stub_user_agent(USER_AGENTS[:chrome])
122
+
123
+ expect(SecureHeaders::ContentSecurityPolicy).to receive(:add_to_env)
124
+ subject.set_csp_header(opts)
144
125
  end
145
126
 
146
127
  context "when disabled by configuration settings" do
@@ -247,7 +228,7 @@ describe SecureHeaders do
247
228
  context "when using Firefox" do
248
229
  it "sets CSP headers" do
249
230
  stub_user_agent(USER_AGENTS[:firefox])
250
- should_assign_header(STANDARD_HEADER_NAME + "-Report-Only", DEFAULT_CSP_HEADER)
231
+ should_assign_header(HEADER_NAME + "-Report-Only", DEFAULT_CSP_HEADER)
251
232
  subject.set_csp_header
252
233
  end
253
234
  end
@@ -255,7 +236,7 @@ describe SecureHeaders do
255
236
  context "when using Chrome" do
256
237
  it "sets default CSP header" do
257
238
  stub_user_agent(USER_AGENTS[:chrome])
258
- should_assign_header(STANDARD_HEADER_NAME + "-Report-Only", DEFAULT_CSP_HEADER)
239
+ should_assign_header(HEADER_NAME + "-Report-Only", DEFAULT_CSP_HEADER)
259
240
  subject.set_csp_header
260
241
  end
261
242
  end
@@ -263,36 +244,9 @@ describe SecureHeaders do
263
244
  context "when using a browser besides chrome/firefox" do
264
245
  it "sets the CSP header" do
265
246
  stub_user_agent(USER_AGENTS[:opera])
266
- should_assign_header(STANDARD_HEADER_NAME + "-Report-Only", DEFAULT_CSP_HEADER)
247
+ should_assign_header(HEADER_NAME + "-Report-Only", DEFAULT_CSP_HEADER)
267
248
  subject.set_csp_header
268
249
  end
269
250
  end
270
-
271
- context "when using the experimental key" do
272
- before(:each) do
273
- stub_user_agent(USER_AGENTS[:chrome])
274
- @opts = {
275
- :enforce => true,
276
- :default_src => 'self',
277
- :script_src => 'https://mycdn.example.com',
278
- :experimental => {
279
- :script_src => 'self',
280
- }
281
- }
282
- end
283
-
284
- it "does not set the header in enforce mode if experimental is supplied, but enforce is disabled" do
285
- opts = @opts.merge(:enforce => false)
286
- should_assign_header(STANDARD_HEADER_NAME + "-Report-Only", anything)
287
- should_not_assign_header(STANDARD_HEADER_NAME)
288
- subject.set_csp_header(opts)
289
- end
290
-
291
- it "sets a header in enforce mode as well as report-only mode" do
292
- should_assign_header(STANDARD_HEADER_NAME, anything)
293
- should_assign_header(STANDARD_HEADER_NAME + "-Report-Only", anything)
294
- subject.set_csp_header(@opts)
295
- end
296
- end
297
251
  end
298
252
  end
data/spec/spec_helper.rb CHANGED
@@ -2,10 +2,34 @@ require 'rubygems'
2
2
  require 'rspec'
3
3
 
4
4
  require File.join(File.dirname(__FILE__), '..', 'lib', 'secure_headers')
5
- require File.join(File.dirname(__FILE__), '..', 'app', 'controllers', 'content_security_policy_controller')
5
+
6
+ if defined?(Coveralls)
7
+ Coveralls.wear!
8
+ end
9
+
6
10
  include ::SecureHeaders::StrictTransportSecurity::Constants
7
11
  include ::SecureHeaders::ContentSecurityPolicy::Constants
8
12
  include ::SecureHeaders::XFrameOptions::Constants
9
13
  include ::SecureHeaders::XXssProtection::Constants
10
14
  include ::SecureHeaders::XContentTypeOptions::Constants
11
15
  include ::SecureHeaders::XDownloadOptions::Constants
16
+
17
+ USER_AGENTS = {
18
+ :firefox => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1',
19
+ :chrome => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.56 Safari/536.5',
20
+ :ie => 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)',
21
+ :opera => 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00',
22
+ :ios5 => "Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3",
23
+ :ios6 => "Mozilla/5.0 (iPhone; CPU iPhone OS 614 like Mac OS X) AppleWebKit/536.26 (KHTML like Gecko) Version/6.0 Mobile/10B350 Safari/8536.25",
24
+ :safari5 => "Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko ) Version/5.1 Mobile/9B176 Safari/7534.48.3",
25
+ :safari5_1 => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10",
26
+ :safari6 => "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/536.30.1 (KHTML like Gecko) Version/6.0.5 Safari/536.30.1"
27
+ }
28
+
29
+ def should_assign_header name, value
30
+ expect(response.headers).to receive(:[]=).with(name, value)
31
+ end
32
+
33
+ def should_not_assign_header name
34
+ expect(response.headers).not_to receive(:[]=).with(name, anything)
35
+ end