secure_headers 1.3.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. data/.gitignore +4 -8
  2. data/.travis.yml +7 -4
  3. data/Gemfile +6 -7
  4. data/README.md +115 -79
  5. data/Rakefile +11 -116
  6. data/fixtures/rails_3_2_12/Gemfile +0 -5
  7. data/fixtures/rails_3_2_12/app/controllers/things_controller.rb +0 -1
  8. data/fixtures/rails_3_2_12/app/views/layouts/application.html.erb +1 -4
  9. data/fixtures/rails_3_2_12/app/views/other_things/index.html.erb +2 -1
  10. data/fixtures/rails_3_2_12/config/application.rb +0 -54
  11. data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +2 -1
  12. data/fixtures/rails_3_2_12/config/routes.rb +0 -57
  13. data/fixtures/rails_3_2_12/config/script_hashes.yml +5 -0
  14. data/fixtures/rails_3_2_12/config.ru +3 -0
  15. data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +55 -18
  16. data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +6 -1
  17. data/fixtures/rails_3_2_12_no_init/Gemfile +0 -6
  18. data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +1 -2
  19. data/fixtures/rails_3_2_12_no_init/app/controllers/things_controller.rb +1 -1
  20. data/fixtures/rails_3_2_12_no_init/app/views/layouts/application.html.erb +0 -2
  21. data/fixtures/rails_3_2_12_no_init/app/views/things/index.html.erb +0 -21
  22. data/fixtures/rails_3_2_12_no_init/config/application.rb +0 -48
  23. data/fixtures/rails_3_2_12_no_init/config/routes.rb +0 -57
  24. data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +5 -0
  25. data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +5 -0
  26. data/fixtures/rails_4_1_8/Gemfile +5 -0
  27. data/fixtures/rails_4_1_8/README.rdoc +28 -0
  28. data/fixtures/rails_4_1_8/Rakefile +6 -0
  29. data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +4 -0
  30. data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
  31. data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +5 -0
  32. data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +5 -0
  33. data/fixtures/rails_4_1_8/app/models/.keep +0 -0
  34. data/fixtures/rails_4_1_8/app/models/concerns/.keep +0 -0
  35. data/fixtures/rails_4_1_8/app/views/layouts/application.html.erb +11 -0
  36. data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +2 -0
  37. data/fixtures/rails_4_1_8/app/views/things/index.html.erb +1 -0
  38. data/fixtures/rails_4_1_8/config/application.rb +15 -0
  39. data/fixtures/rails_4_1_8/config/boot.rb +4 -0
  40. data/fixtures/rails_4_1_8/config/environment.rb +5 -0
  41. data/fixtures/rails_4_1_8/config/environments/test.rb +10 -0
  42. data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +17 -0
  43. data/fixtures/rails_4_1_8/config/routes.rb +4 -0
  44. data/fixtures/rails_4_1_8/config/script_hashes.yml +5 -0
  45. data/fixtures/rails_4_1_8/config/secrets.yml +22 -0
  46. data/fixtures/rails_4_1_8/config.ru +4 -0
  47. data/fixtures/rails_4_1_8/lib/assets/.keep +0 -0
  48. data/fixtures/rails_4_1_8/lib/tasks/.keep +0 -0
  49. data/fixtures/rails_4_1_8/log/.keep +0 -0
  50. data/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +83 -0
  51. data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +59 -0
  52. data/fixtures/rails_4_1_8/spec/spec_helper.rb +15 -0
  53. data/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep +0 -0
  54. data/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep +0 -0
  55. data/lib/secure_headers/hash_helper.rb +7 -0
  56. data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +22 -0
  57. data/lib/secure_headers/headers/content_security_policy.rb +141 -133
  58. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +40 -0
  59. data/lib/secure_headers/railtie.rb +13 -22
  60. data/lib/secure_headers/version.rb +1 -1
  61. data/lib/secure_headers/view_helper.rb +68 -0
  62. data/lib/secure_headers.rb +58 -16
  63. data/lib/tasks/tasks.rake +48 -0
  64. data/secure_headers.gemspec +4 -4
  65. data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +47 -0
  66. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +83 -208
  67. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +1 -1
  68. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +64 -0
  69. data/spec/lib/secure_headers_spec.rb +36 -61
  70. data/spec/spec_helper.rb +26 -1
  71. metadata +51 -37
  72. checksums.yaml +0 -15
  73. data/Guardfile +0 -6
  74. data/HISTORY.md +0 -162
  75. data/app/controllers/content_security_policy_controller.rb +0 -75
  76. data/config/curl-ca-bundle.crt +0 -5420
  77. data/config/routes.rb +0 -3
  78. data/fixtures/rails_3_2_12/config/environments/development.rb +0 -37
  79. data/fixtures/rails_3_2_12/config/environments/production.rb +0 -67
  80. data/fixtures/rails_3_2_12/config/initializers/backtrace_silencers.rb +0 -7
  81. data/fixtures/rails_3_2_12/config/initializers/inflections.rb +0 -15
  82. data/fixtures/rails_3_2_12/config/initializers/mime_types.rb +0 -5
  83. data/fixtures/rails_3_2_12/config/initializers/secret_token.rb +0 -7
  84. data/fixtures/rails_3_2_12/config/initializers/session_store.rb +0 -8
  85. data/fixtures/rails_3_2_12/config/initializers/wrap_parameters.rb +0 -14
  86. data/fixtures/rails_3_2_12/config/locales/en.yml +0 -5
  87. data/fixtures/rails_3_2_12_no_init/app/views/things/_form.html.erb +0 -17
  88. data/fixtures/rails_3_2_12_no_init/app/views/things/edit.html.erb +0 -6
  89. data/fixtures/rails_3_2_12_no_init/app/views/things/new.html.erb +0 -5
  90. data/fixtures/rails_3_2_12_no_init/app/views/things/show.html.erb +0 -5
  91. data/fixtures/rails_3_2_12_no_init/config/environments/development.rb +0 -37
  92. data/fixtures/rails_3_2_12_no_init/config/environments/production.rb +0 -67
  93. data/fixtures/rails_3_2_12_no_init/config/initializers/backtrace_silencers.rb +0 -7
  94. data/fixtures/rails_3_2_12_no_init/config/initializers/inflections.rb +0 -15
  95. data/fixtures/rails_3_2_12_no_init/config/initializers/mime_types.rb +0 -5
  96. data/fixtures/rails_3_2_12_no_init/config/initializers/secret_token.rb +0 -7
  97. data/fixtures/rails_3_2_12_no_init/config/initializers/session_store.rb +0 -8
  98. data/fixtures/rails_3_2_12_no_init/config/initializers/wrap_parameters.rb +0 -14
  99. data/fixtures/rails_3_2_12_no_init/config/locales/en.yml +0 -5
  100. data/spec/controllers/content_security_policy_controller_spec.rb +0 -90
@@ -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
@@ -24,7 +24,7 @@ module SecureHeaders
24
24
 
25
25
  it "doesn't accept anything besides noopen" do
26
26
  expect {
27
- XContentTypeOptions.new("open")
27
+ XDownloadOptions.new("open")
28
28
  }.to raise_error
29
29
  end
30
30
  end
@@ -0,0 +1,64 @@
1
+ module SecureHeaders
2
+ describe XPermittedCrossDomainPolicies do
3
+ specify { expect(XPermittedCrossDomainPolicies.new.name).to eq(XPermittedCrossDomainPolicies::Constants::XPCDP_HEADER_NAME)}
4
+ specify { expect(XPermittedCrossDomainPolicies.new.value).to eq("none")}
5
+ specify { expect(XPermittedCrossDomainPolicies.new('master-only').value).to eq('master-only')}
6
+ specify { expect(XPermittedCrossDomainPolicies.new(:value => 'master-only').value).to eq('master-only') }
7
+
8
+ context "valid configuration values" do
9
+ it "accepts 'all'" do
10
+ expect {
11
+ XPermittedCrossDomainPolicies.new("all")
12
+ }.not_to raise_error
13
+
14
+ expect {
15
+ XPermittedCrossDomainPolicies.new(:value => "all")
16
+ }.not_to raise_error
17
+ end
18
+
19
+ it "accepts 'by-ftp-filename'" do
20
+ expect {
21
+ XPermittedCrossDomainPolicies.new("by-ftp-filename")
22
+ }.not_to raise_error
23
+
24
+ expect {
25
+ XPermittedCrossDomainPolicies.new(:value => "by-ftp-filename")
26
+ }.not_to raise_error
27
+ end
28
+
29
+ it "accepts 'by-content-type'" do
30
+ expect {
31
+ XPermittedCrossDomainPolicies.new("by-content-type")
32
+ }.not_to raise_error
33
+
34
+ expect {
35
+ XPermittedCrossDomainPolicies.new(:value => "by-content-type")
36
+ }.not_to raise_error
37
+ end
38
+ it "accepts 'master-only'" do
39
+ expect {
40
+ XPermittedCrossDomainPolicies.new("master-only")
41
+ }.not_to raise_error
42
+
43
+ expect {
44
+ XPermittedCrossDomainPolicies.new(:value => "master-only")
45
+ }.not_to raise_error
46
+ end
47
+
48
+ it "accepts nil" do
49
+ expect {
50
+ XPermittedCrossDomainPolicies.new
51
+ }.not_to raise_error
52
+ end
53
+ end
54
+
55
+ context 'invlaid configuration values' do
56
+
57
+ it "doesn't accept invalid values" do
58
+ expect {
59
+ XPermittedCrossDomainPolicies.new("open")
60
+ }.to raise_error
61
+ end
62
+ end
63
+ end
64
+ end
@@ -18,26 +18,7 @@ describe SecureHeaders do
18
18
  allow(subject).to receive(:request).and_return(request)
19
19
  end
20
20
 
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
21
+ ALL_HEADERS = Hash[[:hsts, :csp, :x_frame_options, :x_content_type_options, :x_xss_protection, :x_permitted_cross_domain_policies].map{|header| [header, false]}]
41
22
 
42
23
  def stub_user_agent val
43
24
  allow(request).to receive_message_chain(:env, :[]).and_return(val)
@@ -55,6 +36,7 @@ describe SecureHeaders do
55
36
  config.x_xss_protection = nil
56
37
  config.csp = nil
57
38
  config.x_download_options = nil
39
+ config.x_permitted_cross_domain_policies = nil
58
40
  end
59
41
  end
60
42
 
@@ -65,14 +47,7 @@ describe SecureHeaders do
65
47
  subject.set_x_content_type_options_header
66
48
  subject.set_x_xss_protection_header
67
49
  subject.set_x_download_options_header
68
- end
69
-
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
50
+ subject.set_x_permitted_cross_domain_policies_header
76
51
  end
77
52
 
78
53
  describe "#set_header" do
@@ -88,13 +63,10 @@ describe SecureHeaders do
88
63
  end
89
64
 
90
65
  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
66
  USER_AGENTS.each do |name, useragent|
95
67
  it "sets all default headers for #{name} (smoke test)" do
96
68
  stub_user_agent(useragent)
97
- number_of_headers = 6
69
+ number_of_headers = 7
98
70
  expect(subject).to receive(:set_header).exactly(number_of_headers).times # a request for a given header
99
71
  subject.set_csp_header
100
72
  subject.set_x_frame_options_header
@@ -102,6 +74,7 @@ describe SecureHeaders do
102
74
  subject.set_x_xss_protection_header
103
75
  subject.set_x_content_type_options_header
104
76
  subject.set_x_download_options_header
77
+ subject.set_x_permitted_cross_domain_policies_header
105
78
  end
106
79
  end
107
80
 
@@ -126,6 +99,11 @@ describe SecureHeaders do
126
99
  subject.set_x_frame_options_header(false)
127
100
  end
128
101
 
102
+ it "does not set the X-Permitted-Cross-Domain-Policies header if disabled" do
103
+ should_not_assign_header(XPCDP_HEADER_NAME)
104
+ subject.set_x_permitted_cross_domain_policies_header(false)
105
+ end
106
+
129
107
  it "does not set the HSTS header if disabled" do
130
108
  should_not_assign_header(HSTS_HEADER_NAME)
131
109
  subject.set_hsts_header(false)
@@ -139,8 +117,19 @@ describe SecureHeaders do
139
117
 
140
118
  it "does not set the CSP header if disabled" do
141
119
  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))
120
+ should_not_assign_header(HEADER_NAME)
121
+ subject.set_csp_header(false)
122
+ end
123
+
124
+ it "saves the options to the env when using script hashes" do
125
+ opts = {
126
+ :default_src => 'self',
127
+ :script_hash_middleware => true
128
+ }
129
+ stub_user_agent(USER_AGENTS[:chrome])
130
+
131
+ expect(SecureHeaders::ContentSecurityPolicy).to receive(:add_to_env)
132
+ subject.set_csp_header(opts)
144
133
  end
145
134
 
146
135
  context "when disabled by configuration settings" do
@@ -152,6 +141,7 @@ describe SecureHeaders do
152
141
  config.x_xss_protection = false
153
142
  config.csp = false
154
143
  config.x_download_options = false
144
+ config.x_permitted_cross_domain_policies = false
155
145
  end
156
146
  expect(subject).not_to receive(:set_header)
157
147
  set_security_headers(subject)
@@ -247,7 +237,7 @@ describe SecureHeaders do
247
237
  context "when using Firefox" do
248
238
  it "sets CSP headers" do
249
239
  stub_user_agent(USER_AGENTS[:firefox])
250
- should_assign_header(STANDARD_HEADER_NAME + "-Report-Only", DEFAULT_CSP_HEADER)
240
+ should_assign_header(HEADER_NAME + "-Report-Only", DEFAULT_CSP_HEADER)
251
241
  subject.set_csp_header
252
242
  end
253
243
  end
@@ -255,7 +245,7 @@ describe SecureHeaders do
255
245
  context "when using Chrome" do
256
246
  it "sets default CSP header" do
257
247
  stub_user_agent(USER_AGENTS[:chrome])
258
- should_assign_header(STANDARD_HEADER_NAME + "-Report-Only", DEFAULT_CSP_HEADER)
248
+ should_assign_header(HEADER_NAME + "-Report-Only", DEFAULT_CSP_HEADER)
259
249
  subject.set_csp_header
260
250
  end
261
251
  end
@@ -263,36 +253,21 @@ describe SecureHeaders do
263
253
  context "when using a browser besides chrome/firefox" do
264
254
  it "sets the CSP header" do
265
255
  stub_user_agent(USER_AGENTS[:opera])
266
- should_assign_header(STANDARD_HEADER_NAME + "-Report-Only", DEFAULT_CSP_HEADER)
256
+ should_assign_header(HEADER_NAME + "-Report-Only", DEFAULT_CSP_HEADER)
267
257
  subject.set_csp_header
268
258
  end
269
259
  end
260
+ end
270
261
 
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
262
+ describe "#set_x_permitted_cross_domain_policies_header" do
263
+ it "sets the X-Permitted-Cross-Domain-Policies header" do
264
+ should_assign_header(XPCDP_HEADER_NAME, SecureHeaders::XPermittedCrossDomainPolicies::Constants::DEFAULT_VALUE)
265
+ subject.set_x_permitted_cross_domain_policies_header
266
+ end
290
267
 
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
268
+ it "allows a custom X-Permitted-Cross-Domain-Policies header" do
269
+ should_assign_header(XPCDP_HEADER_NAME, "master-only")
270
+ subject.set_x_permitted_cross_domain_policies_header(:value => 'master-only')
296
271
  end
297
272
  end
298
273
  end