secure_headers 1.1.1 → 1.3.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.
- checksums.yaml +15 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +2 -0
- data/Gemfile +0 -4
- data/Guardfile +1 -5
- data/HISTORY.md +12 -0
- data/README.md +55 -26
- data/fixtures/rails_3_2_12/Gemfile +0 -2
- data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +1 -0
- data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +10 -14
- data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +7 -12
- data/fixtures/rails_3_2_12/spec/spec_helper.rb +1 -5
- data/fixtures/rails_3_2_12_no_init/Gemfile +0 -2
- data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +7 -12
- data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +7 -12
- data/fixtures/rails_3_2_12_no_init/spec/spec_helper.rb +3 -18
- data/lib/secure_headers/headers/content_security_policy.rb +39 -34
- data/lib/secure_headers/padrino.rb +14 -0
- data/lib/secure_headers/version.rb +1 -1
- data/spec/controllers/content_security_policy_controller_spec.rb +19 -19
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +105 -85
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +20 -20
- data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +12 -12
- data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +12 -12
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +18 -18
- data/spec/lib/secure_headers_spec.rb +11 -11
- data/spec/spec_helper.rb +9 -24
- metadata +8 -12
- data/.rvmrc +0 -1
- data/fixtures/rails_3_2_12/Guardfile +0 -14
- data/fixtures/rails_3_2_12_no_init/Guardfile +0 -14
checksums.yaml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
!binary "U0hBMQ==":
|
|
3
|
+
metadata.gz: !binary |-
|
|
4
|
+
NTdiMjdmYmVhNzk2MWQ3YmEwNDNjOWQ0YmZhZDZkNTAyNDlmYmQ1ZQ==
|
|
5
|
+
data.tar.gz: !binary |-
|
|
6
|
+
ZTIyYjBjNmM5ZTU5YzRhZjU4MjdmNTcwNDAzMjMyZjJlOTFjOWYxMA==
|
|
7
|
+
SHA512:
|
|
8
|
+
metadata.gz: !binary |-
|
|
9
|
+
NzRjM2QwM2IwZjIyZDMzYzY3OWE2MGVmZTEyNjU3MmQwMzhiODcxYjRlZDlh
|
|
10
|
+
ODFmMmExNzgxNDkzN2U5YWFiNGFjYWMwYjUxNGNiMmJiOWM4ZTA0ZmY3Y2Mz
|
|
11
|
+
MjFlMTRlNzdiMGQ0MDY5NTM1YTkyNGMxOGJjMDRmMjkyZTIyMjA=
|
|
12
|
+
data.tar.gz: !binary |-
|
|
13
|
+
N2I3MzExNWM5MGFjZDI3YjBjMDlkNjgyNWUyODcyZGU3N2NmMjVjZTZkMWVi
|
|
14
|
+
Y2FiYmI4YjY3NzRkYzM3NWNkNjE0MGE4ZWMwMjhlMGJhYTI0NmIwYmY4MGFi
|
|
15
|
+
YjlhNjdkMTQ1YWQ0NTYwNGJiZmMxMzA3MjlkNmMyMDgyZjhlY2E=
|
data/.ruby-gemset
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
secureheaders
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby-1.9.3-p484
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
|
@@ -7,14 +7,10 @@ group :test do
|
|
|
7
7
|
gem 'sqlite3', :platform => [:ruby, :mswin, :mingw]
|
|
8
8
|
gem 'jdbc-sqlite3', :platform => :jruby
|
|
9
9
|
gem 'rspec-rails'
|
|
10
|
-
gem 'spork'
|
|
11
|
-
gem 'pry'
|
|
12
10
|
gem 'rspec'
|
|
13
|
-
gem 'guard-spork', :platform => :ruby_19
|
|
14
11
|
gem 'guard-rspec', :platform => :ruby_19
|
|
15
12
|
gem 'growl'
|
|
16
13
|
gem 'rb-fsevent'
|
|
17
|
-
gem 'simplecov'
|
|
18
14
|
gem 'debugger', :platform => :ruby_19
|
|
19
15
|
gem 'ruby-debug', :platform => :ruby_18
|
|
20
16
|
end
|
data/Guardfile
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
guard '
|
|
2
|
-
watch('spec/spec_helper.rb') { :rspec }
|
|
3
|
-
end
|
|
4
|
-
|
|
5
|
-
guard 'rspec', :cli => "--color --drb --debug", :keep_failed => true, :all_after_pass => true, :focus_on_failed => true do
|
|
1
|
+
guard 'rspec' do
|
|
6
2
|
watch(%r{^spec/.+_spec\.rb$})
|
|
7
3
|
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
|
8
4
|
watch(%r{^app/controllers/(.+)\.rb$}) { |m| "spec/controllers/#{m[1]}_spec.rb" }
|
data/HISTORY.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
1.3.0
|
|
2
|
+
======
|
|
3
|
+
|
|
4
|
+
- CSP nonce support was added back and is compliant.
|
|
5
|
+
- Bugs:
|
|
6
|
+
-- enforce, disable_fill_missing, and disable_chrome_extension did not accept lambdas for no good reason
|
|
7
|
+
-- IF a default-src was specified, and an img-src was not, and disable_fill_missing was true, the img-src value would be :data
|
|
8
|
+
|
|
9
|
+
1.2.0
|
|
10
|
+
======
|
|
11
|
+
- Allow procs to be used as config values.
|
|
12
|
+
|
|
1
13
|
1.1.1
|
|
2
14
|
======
|
|
3
15
|
|
data/README.md
CHANGED
|
@@ -85,9 +85,10 @@ Or simply add it to application controller
|
|
|
85
85
|
|
|
86
86
|
```ruby
|
|
87
87
|
ensure_security_headers(
|
|
88
|
-
:hsts => {:include_subdomains, :
|
|
88
|
+
:hsts => {:include_subdomains => true, :max_age => 20.years.to_i},
|
|
89
89
|
:x_frame_options => 'DENY',
|
|
90
|
-
:csp => false
|
|
90
|
+
:csp => false
|
|
91
|
+
)
|
|
91
92
|
```
|
|
92
93
|
|
|
93
94
|
## Options for ensure\_security\_headers
|
|
@@ -162,13 +163,6 @@ and [Mozilla CSP specification](https://wiki.mozilla.org/Security/CSP/Specificat
|
|
|
162
163
|
:img_src => 'http://mycdn.example.com'
|
|
163
164
|
}
|
|
164
165
|
}
|
|
165
|
-
|
|
166
|
-
# script-nonce is an experimental feature of CSP 1.1 available in Chrome. It allows
|
|
167
|
-
# you to whitelist inline script blocks. For more information, see
|
|
168
|
-
# https://dvcs.w3.org/hg/content-security-policy/raw-file/tip/csp-specification.dev.html#script-nonce
|
|
169
|
-
:script_nonce => lambda { @script_nonce = SecureRandom.hex }
|
|
170
|
-
# which can be used to whitelist a script block:
|
|
171
|
-
# script_tag :nonce = @script_nonce { inline_script_call() }
|
|
172
166
|
}
|
|
173
167
|
```
|
|
174
168
|
|
|
@@ -203,9 +197,36 @@ and [Mozilla CSP specification](https://wiki.mozilla.org/Security/CSP/Specificat
|
|
|
203
197
|
"default-src 'self'; img-src *; object-src media1.com media2.com *.cdn.com; script-src trustedscripts.example.com;"
|
|
204
198
|
```
|
|
205
199
|
|
|
206
|
-
|
|
200
|
+
### CSP Level 2 features
|
|
207
201
|
|
|
208
|
-
|
|
202
|
+
script/style-nonce can be used to whitelist inline content. To do this, add "nonce" to your script/style-src configuration, then set the nonce attributes on the various tags.
|
|
203
|
+
|
|
204
|
+
*setting a nonce will also set 'unsafe-inline' for browsers that don't support nonces for backwards compatibility. 'unsafe-inline' is ignored if a nonce is present in a directive in compliant browsers.
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
:csp => {
|
|
208
|
+
:default_src => 'self',
|
|
209
|
+
:script_src => 'self nonce'
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
> content-security-policy: default-src 'self'; script-src 'self' 'nonce-abc123' 'unsafe-inline'
|
|
214
|
+
|
|
215
|
+
```erb
|
|
216
|
+
<script nonce="<%= @content_security_policy_nonce %>">
|
|
217
|
+
console.log("whitelisted, will execute")
|
|
218
|
+
</script>
|
|
219
|
+
|
|
220
|
+
<script nonce="lol">
|
|
221
|
+
console.log("won't execute, not whitelisted")
|
|
222
|
+
</script>
|
|
223
|
+
|
|
224
|
+
<script>
|
|
225
|
+
console.log("won't execute, not whitelisted")
|
|
226
|
+
</script>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Note on Firefox handling of CSP
|
|
209
230
|
|
|
210
231
|
* CSP reports will not POST cross\-origin. This sets up an internal endpoint in the application that will forward the request. Set the `forward_endpoint` value in the CSP section if you need to post cross origin for firefox. The internal endpoint that receives the initial request will forward the request to `forward_endpoint`
|
|
211
232
|
|
|
@@ -274,22 +295,11 @@ In your `Gemfile`:
|
|
|
274
295
|
then in your `app.rb` file you can:
|
|
275
296
|
|
|
276
297
|
```ruby
|
|
298
|
+
require 'secure_headers/padrino'
|
|
299
|
+
|
|
277
300
|
module Web
|
|
278
301
|
class App < Padrino::Application
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
::SecureHeaders::Configuration.configure do |config|
|
|
282
|
-
config.hsts = {:max_age => 99, :include_subdomains => true}
|
|
283
|
-
config.x_frame_options = 'DENY'
|
|
284
|
-
config.x_content_type_options = "nosniff"
|
|
285
|
-
config.x_xss_protection = {:value => '1', :mode => false}
|
|
286
|
-
config.csp = {
|
|
287
|
-
:default_src => "https://* inline eval",
|
|
288
|
-
:report_uri => '//example.com/uri-directive',
|
|
289
|
-
:img_src => "https://* data:",
|
|
290
|
-
:frame_src => "https://* http://*.twimg.com http://itunes.apple.com"
|
|
291
|
-
}
|
|
292
|
-
end
|
|
302
|
+
register SecureHeaders::Padrino
|
|
293
303
|
|
|
294
304
|
get '/' do
|
|
295
305
|
set_csp_header
|
|
@@ -299,12 +309,31 @@ module Web
|
|
|
299
309
|
end
|
|
300
310
|
```
|
|
301
311
|
|
|
312
|
+
and in `config/boot.rb`:
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
def before_load
|
|
316
|
+
::SecureHeaders::Configuration.configure do |config|
|
|
317
|
+
config.hsts = {:max_age => 99, :include_subdomains => true}
|
|
318
|
+
config.x_frame_options = 'DENY'
|
|
319
|
+
config.x_content_type_options = "nosniff"
|
|
320
|
+
config.x_xss_protection = {:value => '1', :mode => false}
|
|
321
|
+
config.csp = {
|
|
322
|
+
:default_src => "https://* inline eval",
|
|
323
|
+
:report_uri => '//example.com/uri-directive',
|
|
324
|
+
:img_src => "https://* data:",
|
|
325
|
+
:frame_src => "https://* http://*.twimg.com http://itunes.apple.com"
|
|
326
|
+
}
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
```
|
|
330
|
+
|
|
302
331
|
## Similar libraries
|
|
303
332
|
|
|
304
333
|
* Node.js (express) [helmet](https://github.com/evilpacket/helmet) and [hood](https://github.com/seanmonstar/hood)
|
|
305
334
|
* J2EE Servlet >= 3.0 [highlines](https://github.com/sourceclear/headlines)
|
|
306
335
|
* ASP.NET - [NWebsec](http://nwebsec.codeplex.com/)
|
|
307
|
-
* Python - [django-csp](https://github.com/mozilla/django-csp/
|
|
336
|
+
* Python - [django-csp](https://github.com/mozilla/django-csp/) + [commonware](https://github.com/jsocol/commonware/)
|
|
308
337
|
* Go - [secureheader](https://github.com/kr/secureheader)
|
|
309
338
|
|
|
310
339
|
## Authors
|
|
@@ -4,11 +4,9 @@ gem 'rails', '3.2.12'
|
|
|
4
4
|
gem 'sqlite3'
|
|
5
5
|
gem 'rspec-rails', '>= 2.0.0'
|
|
6
6
|
gem 'secure_headers', :path => '../..'
|
|
7
|
-
gem 'spork'
|
|
8
7
|
gem 'debugger', :platform => :ruby_19
|
|
9
8
|
gem 'ruby-debug', :platform => :ruby_18
|
|
10
9
|
gem 'guard-rspec'
|
|
11
|
-
gem 'guard-spork'
|
|
12
10
|
gem 'rb-fsevent'
|
|
13
11
|
gem 'growl'
|
|
14
12
|
|
|
@@ -1,44 +1,40 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
2
|
|
|
3
|
-
describe OtherThingsController do
|
|
3
|
+
describe OtherThingsController, :type => :controller do
|
|
4
4
|
describe "headers" do
|
|
5
|
-
before(:each) do
|
|
6
|
-
# Chrome
|
|
7
|
-
request.env['HTTP_USER_AGENT'] = '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'
|
|
8
|
-
end
|
|
9
|
-
|
|
10
5
|
it "sets the X-XSS-Protection header" do
|
|
11
6
|
get :index
|
|
12
|
-
response.headers['X-XSS-Protection'].
|
|
7
|
+
expect(response.headers['X-XSS-Protection']).to eq('1; mode=block')
|
|
13
8
|
end
|
|
14
9
|
|
|
15
10
|
it "sets the X-Frame-Options header" do
|
|
16
11
|
get :index
|
|
17
|
-
response.headers['X-Frame-Options'].
|
|
12
|
+
expect(response.headers['X-Frame-Options']).to eq('SAMEORIGIN')
|
|
18
13
|
end
|
|
19
14
|
|
|
20
|
-
it "sets the
|
|
15
|
+
it "sets the CSP header with a local reference to a nonce" do
|
|
21
16
|
get :index
|
|
22
|
-
|
|
17
|
+
nonce = controller.instance_exec { @content_security_policy_nonce }
|
|
18
|
+
expect(nonce).to match /[a-zA-Z0-9\+\/=]{44}/
|
|
19
|
+
expect(response.headers['Content-Security-Policy-Report-Only']).to match(/default-src 'self'; img-src 'self' data:; script-src 'self' 'nonce-[a-zA-Z0-9\+\/=]{44}' 'unsafe-inline'; report-uri somewhere;/)
|
|
23
20
|
end
|
|
24
21
|
|
|
25
|
-
#mock ssl
|
|
26
22
|
it "sets the Strict-Transport-Security header" do
|
|
27
23
|
request.env['HTTPS'] = 'on'
|
|
28
24
|
get :index
|
|
29
|
-
response.headers['Strict-Transport-Security'].
|
|
25
|
+
expect(response.headers['Strict-Transport-Security']).to eq("max-age=315576000")
|
|
30
26
|
end
|
|
31
27
|
|
|
32
28
|
it "sets the X-Content-Type-Options header" do
|
|
33
29
|
get :index
|
|
34
|
-
response.headers['X-Content-Type-Options'].
|
|
30
|
+
expect(response.headers['X-Content-Type-Options']).to eq("nosniff")
|
|
35
31
|
end
|
|
36
32
|
|
|
37
33
|
context "using IE" do
|
|
38
34
|
it "sets the X-Content-Type-Options header" do
|
|
39
35
|
request.env['HTTP_USER_AGENT'] = "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0"
|
|
40
36
|
get :index
|
|
41
|
-
response.headers['X-Content-Type-Options'].
|
|
37
|
+
expect(response.headers['X-Content-Type-Options']).to eq("nosniff")
|
|
42
38
|
end
|
|
43
39
|
end
|
|
44
40
|
end
|
|
@@ -4,45 +4,40 @@ require 'spec_helper'
|
|
|
4
4
|
# all values are defaulted because no initializer is configured, and the values in app controller
|
|
5
5
|
# only provide csp => false
|
|
6
6
|
|
|
7
|
-
describe ThingsController do
|
|
7
|
+
describe ThingsController, :type => :controller do
|
|
8
8
|
describe "headers" do
|
|
9
|
-
before(:each) do
|
|
10
|
-
# Chrome
|
|
11
|
-
request.env['HTTP_USER_AGENT'] = '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'
|
|
12
|
-
end
|
|
13
|
-
|
|
14
9
|
it "sets the X-XSS-Protection header" do
|
|
15
10
|
get :index
|
|
16
|
-
response.headers['X-XSS-Protection'].
|
|
11
|
+
expect(response.headers['X-XSS-Protection']).to eq('1; mode=block')
|
|
17
12
|
end
|
|
18
13
|
|
|
19
14
|
it "sets the X-Frame-Options header" do
|
|
20
15
|
get :index
|
|
21
|
-
response.headers['X-Frame-Options'].
|
|
16
|
+
expect(response.headers['X-Frame-Options']).to eq('SAMEORIGIN')
|
|
22
17
|
end
|
|
23
18
|
|
|
24
19
|
it "sets the X-WebKit-CSP header" do
|
|
25
20
|
get :index
|
|
26
|
-
response.headers['Content-Security-Policy-Report-Only'].
|
|
21
|
+
expect(response.headers['Content-Security-Policy-Report-Only']).to eq(nil)
|
|
27
22
|
end
|
|
28
23
|
|
|
29
24
|
#mock ssl
|
|
30
25
|
it "sets the Strict-Transport-Security header" do
|
|
31
26
|
request.env['HTTPS'] = 'on'
|
|
32
27
|
get :index
|
|
33
|
-
response.headers['Strict-Transport-Security'].
|
|
28
|
+
expect(response.headers['Strict-Transport-Security']).to eq("max-age=315576000")
|
|
34
29
|
end
|
|
35
30
|
|
|
36
31
|
it "sets the X-Content-Type-Options header" do
|
|
37
32
|
get :index
|
|
38
|
-
response.headers['X-Content-Type-Options'].
|
|
33
|
+
expect(response.headers['X-Content-Type-Options']).to eq("nosniff")
|
|
39
34
|
end
|
|
40
35
|
|
|
41
36
|
context "using IE" do
|
|
42
37
|
it "sets the X-Content-Type-Options header" do
|
|
43
38
|
request.env['HTTP_USER_AGENT'] = "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0"
|
|
44
39
|
get :index
|
|
45
|
-
response.headers['X-Content-Type-Options'].
|
|
40
|
+
expect(response.headers['X-Content-Type-Options']).to eq("nosniff")
|
|
46
41
|
end
|
|
47
42
|
end
|
|
48
43
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require 'rubygems'
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
#uncomment the following line to use spork with the debugger
|
|
4
4
|
#require 'spork/ext/ruby-debug'
|
|
5
5
|
|
|
@@ -11,9 +11,5 @@ require 'spork'
|
|
|
11
11
|
ENV["RAILS_ENV"] ||= 'test'
|
|
12
12
|
require File.expand_path("../../config/environment", __FILE__)
|
|
13
13
|
require 'rspec/rails'
|
|
14
|
-
require 'rspec/autorun'
|
|
15
14
|
# end
|
|
16
15
|
|
|
17
|
-
Spork.each_run do
|
|
18
|
-
|
|
19
|
-
end
|
|
@@ -4,11 +4,9 @@ gem 'rails', '3.2.12'
|
|
|
4
4
|
gem 'sqlite3'
|
|
5
5
|
gem 'rspec-rails', '>= 2.0.0'
|
|
6
6
|
gem 'secure_headers', :path => '../..'
|
|
7
|
-
gem 'spork'
|
|
8
7
|
gem 'debugger', :platform => :ruby_19
|
|
9
8
|
gem 'ruby-debug', :platform => :ruby_18
|
|
10
9
|
gem 'guard-rspec'
|
|
11
|
-
gem 'guard-spork'
|
|
12
10
|
gem 'rb-fsevent'
|
|
13
11
|
gem 'growl'
|
|
14
12
|
|
|
@@ -1,44 +1,39 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
2
|
|
|
3
|
-
describe OtherThingsController do
|
|
3
|
+
describe OtherThingsController, :type => :controller do
|
|
4
4
|
describe "headers" do
|
|
5
|
-
before(:each) do
|
|
6
|
-
# Chrome
|
|
7
|
-
request.env['HTTP_USER_AGENT'] = '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'
|
|
8
|
-
end
|
|
9
|
-
|
|
10
5
|
it "sets the X-XSS-Protection header" do
|
|
11
6
|
get :index
|
|
12
|
-
response.headers['X-XSS-Protection'].
|
|
7
|
+
expect(response.headers['X-XSS-Protection']).to eq(SecureHeaders::XXssProtection::Constants::DEFAULT_VALUE)
|
|
13
8
|
end
|
|
14
9
|
|
|
15
10
|
it "sets the X-Frame-Options header" do
|
|
16
11
|
get :index
|
|
17
|
-
response.headers['X-Frame-Options'].
|
|
12
|
+
expect(response.headers['X-Frame-Options']).to eq(SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE)
|
|
18
13
|
end
|
|
19
14
|
|
|
20
15
|
it "sets the X-WebKit-CSP header" do
|
|
21
16
|
get :index
|
|
22
|
-
response.headers['Content-Security-Policy-Report-Only'].
|
|
17
|
+
expect(response.headers['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; img-src 'self' data:;")
|
|
23
18
|
end
|
|
24
19
|
|
|
25
20
|
#mock ssl
|
|
26
21
|
it "sets the Strict-Transport-Security header" do
|
|
27
22
|
request.env['HTTPS'] = 'on'
|
|
28
23
|
get :index
|
|
29
|
-
response.headers['Strict-Transport-Security'].
|
|
24
|
+
expect(response.headers['Strict-Transport-Security']).to eq(SecureHeaders::StrictTransportSecurity::Constants::DEFAULT_VALUE)
|
|
30
25
|
end
|
|
31
26
|
|
|
32
27
|
it "sets the X-Content-Type-Options header" do
|
|
33
28
|
get :index
|
|
34
|
-
response.headers['X-Content-Type-Options'].
|
|
29
|
+
expect(response.headers['X-Content-Type-Options']).to eq(SecureHeaders::XContentTypeOptions::Constants::DEFAULT_VALUE)
|
|
35
30
|
end
|
|
36
31
|
|
|
37
32
|
context "using IE" do
|
|
38
33
|
it "sets the X-Content-Type-Options header" do
|
|
39
34
|
request.env['HTTP_USER_AGENT'] = "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0"
|
|
40
35
|
get :index
|
|
41
|
-
response.headers['X-Content-Type-Options'].
|
|
36
|
+
expect(response.headers['X-Content-Type-Options']).to eq(SecureHeaders::XContentTypeOptions::Constants::DEFAULT_VALUE)
|
|
42
37
|
end
|
|
43
38
|
end
|
|
44
39
|
end
|
|
@@ -4,45 +4,40 @@ require 'spec_helper'
|
|
|
4
4
|
# all values are defaulted because no initializer is configured, and the values in app controller
|
|
5
5
|
# only provide csp => false
|
|
6
6
|
|
|
7
|
-
describe ThingsController do
|
|
7
|
+
describe ThingsController, :type => :controller do
|
|
8
8
|
describe "headers" do
|
|
9
|
-
before(:each) do
|
|
10
|
-
# Chrome
|
|
11
|
-
request.env['HTTP_USER_AGENT'] = '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'
|
|
12
|
-
end
|
|
13
|
-
|
|
14
9
|
it "sets the X-XSS-Protection header" do
|
|
15
10
|
get :index
|
|
16
|
-
response.headers['X-XSS-Protection'].
|
|
11
|
+
expect(response.headers['X-XSS-Protection']).to eq(SecureHeaders::XXssProtection::Constants::DEFAULT_VALUE)
|
|
17
12
|
end
|
|
18
13
|
|
|
19
14
|
it "sets the X-Frame-Options header" do
|
|
20
15
|
get :index
|
|
21
|
-
response.headers['X-Frame-Options'].
|
|
16
|
+
expect(response.headers['X-Frame-Options']).to eq(SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE)
|
|
22
17
|
end
|
|
23
18
|
|
|
24
19
|
it "sets the X-WebKit-CSP header" do
|
|
25
20
|
get :index
|
|
26
|
-
response.headers['Content-Security-Policy-Report-Only'].
|
|
21
|
+
expect(response.headers['Content-Security-Policy-Report-Only']).to eq(nil)
|
|
27
22
|
end
|
|
28
23
|
|
|
29
24
|
#mock ssl
|
|
30
25
|
it "sets the Strict-Transport-Security header" do
|
|
31
26
|
request.env['HTTPS'] = 'on'
|
|
32
27
|
get :index
|
|
33
|
-
response.headers['Strict-Transport-Security'].
|
|
28
|
+
expect(response.headers['Strict-Transport-Security']).to eq(SecureHeaders::StrictTransportSecurity::Constants::DEFAULT_VALUE)
|
|
34
29
|
end
|
|
35
30
|
|
|
36
31
|
it "sets the X-Content-Type-Options header" do
|
|
37
32
|
get :index
|
|
38
|
-
response.headers['X-Content-Type-Options'].
|
|
33
|
+
expect(response.headers['X-Content-Type-Options']).to eq(SecureHeaders::XContentTypeOptions::Constants::DEFAULT_VALUE)
|
|
39
34
|
end
|
|
40
35
|
|
|
41
36
|
context "using IE" do
|
|
42
37
|
it "sets the X-Content-Type-Options header" do
|
|
43
38
|
request.env['HTTP_USER_AGENT'] = "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0"
|
|
44
39
|
get :index
|
|
45
|
-
response.headers['X-Content-Type-Options'].
|
|
40
|
+
expect(response.headers['X-Content-Type-Options']).to eq(SecureHeaders::XContentTypeOptions::Constants::DEFAULT_VALUE)
|
|
46
41
|
end
|
|
47
42
|
end
|
|
48
43
|
end
|
|
@@ -1,20 +1,5 @@
|
|
|
1
1
|
require 'rubygems'
|
|
2
|
-
require 'spork'
|
|
3
|
-
#uncomment the following line to use spork with the debugger
|
|
4
|
-
#require 'spork/ext/ruby-debug'
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
# need to restart spork for it take effect.
|
|
10
|
-
# This file is copied to spec/ when you run 'rails generate rspec:install'
|
|
11
|
-
ENV["RAILS_ENV"] ||= 'test'
|
|
12
|
-
require File.expand_path("../../config/environment", __FILE__)
|
|
13
|
-
require 'rspec/rails'
|
|
14
|
-
require 'rspec/autorun'
|
|
15
|
-
# require 'ruby-debug'
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
Spork.each_run do
|
|
19
|
-
|
|
20
|
-
end
|
|
3
|
+
ENV["RAILS_ENV"] ||= 'test'
|
|
4
|
+
require File.expand_path("../../config/environment", __FILE__)
|
|
5
|
+
require 'rspec/rails'
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'uri'
|
|
2
|
+
require 'base64'
|
|
2
3
|
|
|
3
4
|
module SecureHeaders
|
|
4
5
|
class ContentSecurityPolicyBuildError < StandardError; end
|
|
@@ -8,7 +9,7 @@ module SecureHeaders
|
|
|
8
9
|
STANDARD_HEADER_NAME = "Content-Security-Policy"
|
|
9
10
|
FF_CSP_ENDPOINT = "/content_security_policy/forward_report"
|
|
10
11
|
DIRECTIVES = [:default_src, :script_src, :frame_src, :style_src, :img_src, :media_src, :font_src, :object_src, :connect_src]
|
|
11
|
-
META = [:
|
|
12
|
+
META = [:disable_chrome_extension, :disable_fill_missing, :forward_endpoint]
|
|
12
13
|
end
|
|
13
14
|
include Constants
|
|
14
15
|
|
|
@@ -30,6 +31,7 @@ module SecureHeaders
|
|
|
30
31
|
def initialize(config=nil, options={})
|
|
31
32
|
@experimental = !!options.delete(:experimental)
|
|
32
33
|
@controller = options.delete(:controller)
|
|
34
|
+
|
|
33
35
|
if options[:request]
|
|
34
36
|
parse_request(options[:request])
|
|
35
37
|
else
|
|
@@ -45,6 +47,10 @@ module SecureHeaders
|
|
|
45
47
|
configure(config) if config
|
|
46
48
|
end
|
|
47
49
|
|
|
50
|
+
def nonce
|
|
51
|
+
@nonce ||= SecureRandom.base64(32).chomp
|
|
52
|
+
end
|
|
53
|
+
|
|
48
54
|
def configure(config)
|
|
49
55
|
@config = config.dup
|
|
50
56
|
|
|
@@ -54,21 +60,24 @@ module SecureHeaders
|
|
|
54
60
|
@config.merge!(experimental_config)
|
|
55
61
|
end
|
|
56
62
|
|
|
63
|
+
# http_additions will be the only field that still doesn't support
|
|
64
|
+
# lambdas because it's an ugly api that's showing it's age.
|
|
65
|
+
@http_additions = @config.delete(:http_additions)
|
|
66
|
+
|
|
67
|
+
normalize_csp_options
|
|
68
|
+
|
|
57
69
|
META.each do |meta|
|
|
58
70
|
self.send("#{meta}=", @config.delete(meta))
|
|
59
71
|
end
|
|
60
72
|
|
|
61
|
-
@
|
|
62
|
-
@script_nonce = @config.delete(:script_nonce)
|
|
63
|
-
|
|
64
|
-
normalize_csp_options
|
|
73
|
+
@enforce = @config.delete(:enforce)
|
|
65
74
|
normalize_reporting_endpoint
|
|
66
75
|
fill_directives unless disable_fill_missing?
|
|
67
76
|
end
|
|
68
77
|
|
|
69
78
|
def name
|
|
70
79
|
base = STANDARD_HEADER_NAME
|
|
71
|
-
if
|
|
80
|
+
if !@enforce || experimental
|
|
72
81
|
base += "-Report-Only"
|
|
73
82
|
end
|
|
74
83
|
base
|
|
@@ -89,16 +98,9 @@ module SecureHeaders
|
|
|
89
98
|
raise "Expected to find default_src directive value" unless @config[:default_src]
|
|
90
99
|
append_http_additions unless ssl_request?
|
|
91
100
|
header_value = [
|
|
92
|
-
# ensure default-src is first
|
|
93
|
-
build_directive(:default_src),
|
|
94
101
|
generic_directives(@config),
|
|
95
|
-
report_uri_directive
|
|
96
|
-
|
|
97
|
-
].join
|
|
98
|
-
|
|
99
|
-
#store the value for next time
|
|
100
|
-
@config = header_value
|
|
101
|
-
header_value.strip
|
|
102
|
+
report_uri_directive
|
|
103
|
+
].join.strip
|
|
102
104
|
rescue StandardError => e
|
|
103
105
|
raise ContentSecurityPolicyBuildError.new("Couldn't build CSP header :( #{e}")
|
|
104
106
|
end
|
|
@@ -115,20 +117,31 @@ module SecureHeaders
|
|
|
115
117
|
end
|
|
116
118
|
|
|
117
119
|
def append_http_additions
|
|
118
|
-
return unless http_additions
|
|
119
|
-
http_additions.each do |k, v|
|
|
120
|
+
return unless @http_additions
|
|
121
|
+
@http_additions.each do |k, v|
|
|
120
122
|
@config[k] ||= []
|
|
121
123
|
@config[k] << v
|
|
122
124
|
end
|
|
123
125
|
end
|
|
124
126
|
|
|
125
127
|
def normalize_csp_options
|
|
126
|
-
@config.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
@config = @config.inject({}) do |hash, (key, value)|
|
|
129
|
+
# lambdas
|
|
130
|
+
config_val = value.respond_to?(:call) ? value.call : value
|
|
131
|
+
# space-delimeted strings
|
|
132
|
+
config_val = config_val.split if config_val.is_a? String
|
|
133
|
+
# array of strings
|
|
134
|
+
if config_val.respond_to?(:map) #skip booleans
|
|
135
|
+
config_val = config_val.map do |val|
|
|
136
|
+
translate_dir_value(val)
|
|
137
|
+
end.flatten.uniq
|
|
130
138
|
end
|
|
139
|
+
|
|
140
|
+
hash[key] = config_val
|
|
141
|
+
hash
|
|
131
142
|
end
|
|
143
|
+
|
|
144
|
+
@report_uri = @config.delete(:report_uri).join(" ") if @config[:report_uri]
|
|
132
145
|
end
|
|
133
146
|
|
|
134
147
|
# translates 'inline','self', 'none' and 'eval' to their respective impl-specific values.
|
|
@@ -138,6 +151,9 @@ module SecureHeaders
|
|
|
138
151
|
# self/none are special sources/src-dir-values and need to be quoted in chrome
|
|
139
152
|
elsif %{self none}.include?(val)
|
|
140
153
|
"'#{val}'"
|
|
154
|
+
elsif val == 'nonce'
|
|
155
|
+
@controller.instance_variable_set(:@content_security_policy_nonce, nonce)
|
|
156
|
+
["'nonce-#{nonce}'", "'unsafe-inline'"]
|
|
141
157
|
else
|
|
142
158
|
val
|
|
143
159
|
end
|
|
@@ -180,26 +196,15 @@ module SecureHeaders
|
|
|
180
196
|
"report-uri #{@report_uri};"
|
|
181
197
|
end
|
|
182
198
|
|
|
183
|
-
def script_nonce_directive
|
|
184
|
-
return '' if @script_nonce.nil?
|
|
185
|
-
nonce_value = if @script_nonce.is_a?(String)
|
|
186
|
-
@script_nonce
|
|
187
|
-
elsif @controller
|
|
188
|
-
@controller.instance_exec(&@script_nonce)
|
|
189
|
-
else
|
|
190
|
-
@script_nonce.call
|
|
191
|
-
end
|
|
192
|
-
"script-nonce #{nonce_value};"
|
|
193
|
-
end
|
|
194
|
-
|
|
195
199
|
def generic_directives(config)
|
|
196
200
|
header_value = ''
|
|
197
201
|
if config[:img_src]
|
|
198
202
|
config[:img_src] = config[:img_src] + ['data:'] unless config[:img_src].include?('data:')
|
|
199
203
|
else
|
|
200
|
-
config[:img_src] = ['data:']
|
|
204
|
+
config[:img_src] = config[:default_src] + ['data:']
|
|
201
205
|
end
|
|
202
206
|
|
|
207
|
+
header_value = build_directive(:default_src)
|
|
203
208
|
config.keys.sort_by{|k| k.to_s}.each do |k| # ensure consistent ordering
|
|
204
209
|
header_value += build_directive(k)
|
|
205
210
|
end
|