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.
- checksums.yaml +7 -0
- data/.gitignore +4 -8
- data/Gemfile +2 -2
- data/Guardfile +8 -0
- data/README.md +102 -48
- data/Rakefile +0 -116
- data/fixtures/rails_3_2_12/app/views/layouts/application.html.erb +1 -1
- data/fixtures/rails_3_2_12/app/views/other_things/index.html.erb +2 -1
- data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +1 -1
- data/fixtures/rails_3_2_12/config/script_hashes.yml +5 -0
- data/fixtures/rails_3_2_12/config.ru +3 -0
- data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +50 -18
- data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +1 -1
- data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +1 -2
- data/lib/secure_headers/hash_helper.rb +7 -0
- data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +22 -0
- data/lib/secure_headers/headers/content_security_policy.rb +141 -137
- data/lib/secure_headers/railtie.rb +0 -22
- data/lib/secure_headers/version.rb +1 -1
- data/lib/secure_headers/view_helper.rb +68 -0
- data/lib/secure_headers.rb +51 -17
- data/lib/tasks/tasks.rake +48 -0
- data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +47 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +83 -208
- data/spec/lib/secure_headers_spec.rb +16 -62
- data/spec/spec_helper.rb +25 -1
- metadata +22 -24
- data/HISTORY.md +0 -162
- data/app/controllers/content_security_policy_controller.rb +0 -76
- data/config/curl-ca-bundle.crt +0 -5420
- data/config/routes.rb +0 -3
- 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
|
12
|
-
:style_src => "inline https
|
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(
|
34
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX).name).to eq(
|
35
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX_23).name).to eq(
|
36
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME).name).to eq(
|
37
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME_25).name).to eq(
|
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(
|
42
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX)).name).to eq(
|
43
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX_23)).name).to eq(
|
44
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME)).name).to eq(
|
45
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME_25)).name).to eq(
|
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(
|
52
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX)).name).to eq(
|
53
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX_23)).name).to eq(
|
54
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME)).name).to eq(
|
55
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME_25)).name).to eq(
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
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.
|
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(
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
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 "
|
380
|
-
|
381
|
-
|
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
|
-
|
385
|
-
|
386
|
-
|
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(
|
143
|
-
subject.set_csp_header(
|
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(
|
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(
|
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(
|
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
|
-
|
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
|