secure_headers 1.4.1 → 2.0.0.pre
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.
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
|