rack-protection 0.1.0

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

Potentially problematic release.


This version of rack-protection might be problematic. Click here for more details.

@@ -0,0 +1,29 @@
1
+ require 'rack/protection'
2
+
3
+ module Rack
4
+ module Protection
5
+ ##
6
+ # Prevented attack:: Directory traversal
7
+ # Supported browsers:: all
8
+ # More infos:: http://en.wikipedia.org/wiki/Directory_traversal
9
+ #
10
+ # Unescapes '/' and '.', expands +path_info+.
11
+ # Thus <tt>GET /foo/%2e%2e%2fbar</tt> becomes <tt>GET /bar</tt>.
12
+ class PathTraversal < Base
13
+ def call(env)
14
+ path_was = env["PATH_INFO"]
15
+ env["PATH_INFO"] = cleanup path_was
16
+ app.call env
17
+ ensure
18
+ env["PATH_INFO"] = path_was
19
+ end
20
+
21
+ def cleanup(path)
22
+ return cleanup("/" << path)[1..-1] unless path[0] == ?/
23
+ escaped = ::File.expand_path path.gsub('%2e', '.').gsub('%2f', '/')
24
+ escaped << '/' if escaped[-1] != ?/ and path =~ /\/\.{0,2}$/
25
+ escaped.gsub /\/\/+/, '/'
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ require 'rack/protection'
2
+
3
+ module Rack
4
+ module Protection
5
+ ##
6
+ # Prevented attack:: CSRF
7
+ # Supported browsers:: all
8
+ # More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery
9
+ #
10
+ # Does not accept unsafe HTTP requests if the Referer [sic] header is set to
11
+ # a different host.
12
+ #
13
+ # Combine with NoReferrer to also block remote requests from non-HTTP pages
14
+ # (FTP/HTTPS/...).
15
+ class RemoteReferrer < Base
16
+ default_reaction :deny
17
+
18
+ def accepts?(env)
19
+ safe?(env) or referrer(env) == Request.new(env).host
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ require 'rack/protection'
2
+
3
+ module Rack
4
+ module Protection
5
+ ##
6
+ # Prevented attack:: CSRF
7
+ # Supported browsers:: all
8
+ # More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery
9
+ #
10
+ # Only accepts unsafe HTTP requests if a given access token matches the token
11
+ # included in the session *or* the request comes from the same origin.
12
+ #
13
+ # Compatible with Rails and rack-csrf.
14
+ class RemoteToken < AuthenticityToken
15
+ default_reaction :deny
16
+
17
+ def accepts?(env)
18
+ super or referrer(env) == Request.new(env).host
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,36 @@
1
+ require 'rack/protection'
2
+
3
+ module Rack
4
+ module Protection
5
+ ##
6
+ # Prevented attack:: Session Hijacking
7
+ # Supported browsers:: all
8
+ # More infos:: http://en.wikipedia.org/wiki/Session_hijacking
9
+ #
10
+ # Tracks request properties like the user agent in the session and empties
11
+ # the session if those properties change. This essentially prevents attacks
12
+ # from Firesheep. Since all headers taken into consideration might be
13
+ # spoofed, too, this will not prevent all hijacking attempts.
14
+ class SessionHijacking < Base
15
+ default_reaction :drop_session
16
+ default_options :tracking_key => :tracking, :encrypt_tracking => true,
17
+ :track => %w[HTTP_USER_AGENT HTTP_ACCEPT_ENCODING HTTP_ACCEPT_LANGUAGE
18
+ HTTP_VERSION]
19
+
20
+ def accepts?(env)
21
+ session = session env
22
+ key = options[:tracking_key]
23
+ if session.include? key
24
+ session[key].all? { |k,v| v == encrypt(env[k]) }
25
+ else
26
+ session[key] = {}
27
+ options[:track].each { |k| session[key][k] = encrypt(env[k]) }
28
+ end
29
+ end
30
+
31
+ def encrypt(value)
32
+ options[:encrypt_tracking] ? super(value) : value.to_s
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,44 @@
1
+ module Rack
2
+ module Protection
3
+ def self.version
4
+ VERSION
5
+ end
6
+
7
+ module VERSION
8
+ extend Comparable
9
+
10
+ MAJOR = 0
11
+ MINOR = 1
12
+ TINY = 0
13
+ SIGNATURE = [MAJOR, MINOR, TINY]
14
+ STRING = SIGNATURE.join '.'
15
+
16
+ def self.major; MAJOR end
17
+ def self.minor; MINOR end
18
+ def self.tiny; TINY end
19
+ def self.to_s; STRING end
20
+
21
+ def self.hash
22
+ STRING.hash
23
+ end
24
+
25
+ def self.<=>(other)
26
+ other = other.split('.').map { |i| i.to_i } if other.respond_to? :split
27
+ SIGNATURE <=> Array(other)
28
+ end
29
+
30
+ def self.inspect
31
+ STRING.inspect
32
+ end
33
+
34
+ def self.respond_to?(meth, *)
35
+ meth.to_s !~ /^__|^to_str$/ and STRING.respond_to? meth unless super
36
+ end
37
+
38
+ def self.method_missing(meth, *args, &block)
39
+ return super unless STRING.respond_to?(meth)
40
+ STRING.send(meth, *args, &block)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,27 @@
1
+ require 'rack/protection'
2
+
3
+ module Rack
4
+ module Protection
5
+ ##
6
+ # Prevented attack:: Non-permanent XSS
7
+ # Supported browsers:: Internet Explorer 8 and later
8
+ # More infos:: http://blogs.msdn.com/b/ie/archive/2008/07/01/ie8-security-part-iv-the-xss-filter.aspx
9
+ #
10
+ # Sets X-XSS-Protection header to tell the browser to block attacks.
11
+ #
12
+ # Options:
13
+ # xss_mode:: How the browser should prevent the attack (default: :block)
14
+ class XSSHeader < Base
15
+ default_options :xss_mode => :block
16
+
17
+ def header
18
+ { 'X-XSS-Protection' => "1; mode=#{options[:xss_mode]}" }
19
+ end
20
+
21
+ def call(env)
22
+ status, headers, body = @app.call(env)
23
+ [status, header.merge(headers), body]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,61 @@
1
+ # Run `rake rack-protection.gemspec` to update the gemspec.
2
+ Gem::Specification.new do |s|
3
+ # general infos
4
+ s.name = "rack-protection"
5
+ s.version = "0.1.0"
6
+ s.description = "You should use protection!"
7
+ s.homepage = "http://github.com/rkh/rack-protection"
8
+ s.summary = s.description
9
+
10
+ # generated from git shortlog -sn
11
+ s.authors = [
12
+ "Konstantin Haase"
13
+ ]
14
+
15
+ # generated from git shortlog -sne
16
+ s.email = [
17
+ "konstantin.mailinglists@googlemail.com"
18
+ ]
19
+
20
+ # generated from git ls-files
21
+ s.files = [
22
+ "License",
23
+ "README.md",
24
+ "Rakefile",
25
+ "lib/rack-protection.rb",
26
+ "lib/rack/protection.rb",
27
+ "lib/rack/protection/authenticity_token.rb",
28
+ "lib/rack/protection/base.rb",
29
+ "lib/rack/protection/escaped_params.rb",
30
+ "lib/rack/protection/form_token.rb",
31
+ "lib/rack/protection/frame_options.rb",
32
+ "lib/rack/protection/ip_spoofing.rb",
33
+ "lib/rack/protection/json_csrf.rb",
34
+ "lib/rack/protection/path_traversal.rb",
35
+ "lib/rack/protection/remote_referrer.rb",
36
+ "lib/rack/protection/remote_token.rb",
37
+ "lib/rack/protection/session_hijacking.rb",
38
+ "lib/rack/protection/version.rb",
39
+ "lib/rack/protection/xss_header.rb",
40
+ "rack-protection.gemspec",
41
+ "spec/authenticity_token_spec.rb",
42
+ "spec/escaped_params_spec.rb",
43
+ "spec/form_token_spec.rb",
44
+ "spec/frame_options_spec.rb",
45
+ "spec/ip_spoofing_spec.rb",
46
+ "spec/json_csrf_spec.rb",
47
+ "spec/path_traversal_spec.rb",
48
+ "spec/protection_spec.rb",
49
+ "spec/remote_referrer_spec.rb",
50
+ "spec/remote_token_spec.rb",
51
+ "spec/session_hijacking_spec.rb",
52
+ "spec/spec_helper.rb",
53
+ "spec/xss_header_spec.rb"
54
+ ]
55
+
56
+ # dependencies
57
+ s.add_dependency "rack"
58
+ s.add_dependency "escape_utils"
59
+ s.add_development_dependency "rack-test"
60
+ s.add_development_dependency "rspec", "~> 2.0"
61
+ end
@@ -0,0 +1,33 @@
1
+ require File.expand_path('../spec_helper.rb', __FILE__)
2
+
3
+ describe Rack::Protection::AuthenticityToken do
4
+ it_behaves_like "any rack application"
5
+
6
+ it "denies post requests without any token" do
7
+ post('/').should_not be_ok
8
+ end
9
+
10
+ it "accepts post requests with correct X-CSRF-Token header" do
11
+ post('/', {}, 'rack.session' => {:csrf => "a"}, 'HTTP_X_CSRF_TOKEN' => "a")
12
+ last_response.should be_ok
13
+ end
14
+
15
+ it "denies post requests with wrong X-CSRF-Token header" do
16
+ post('/', {}, 'rack.session' => {:csrf => "a"}, 'HTTP_X_CSRF_TOKEN' => "b")
17
+ last_response.should_not be_ok
18
+ end
19
+
20
+ it "accepts post form requests with correct authenticity_token field" do
21
+ post('/', {"authenticity_token" => "a"}, 'rack.session' => {:csrf => "a"})
22
+ last_response.should be_ok
23
+ end
24
+
25
+ it "denies post form requests with wrong authenticity_token field" do
26
+ post('/', {"authenticity_token" => "a"}, 'rack.session' => {:csrf => "b"})
27
+ last_response.should_not be_ok
28
+ end
29
+
30
+ it "prevents ajax requests without a valid token" do
31
+ post('/', {}, "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest").should_not be_ok
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ require File.expand_path('../spec_helper.rb', __FILE__)
2
+
3
+ describe Rack::Protection::EscapedParams do
4
+ it_behaves_like "any rack application"
5
+
6
+ context 'escaping' do
7
+ it 'escapes html entities' do
8
+ mock_app do |env|
9
+ request = Rack::Request.new(env)
10
+ [200, {'Content-Type' => 'text/plain'}, [request.params['foo']]]
11
+ end
12
+ get '/', :foo => "<bar>"
13
+ body.should == '&lt;bar&gt;'
14
+ end
15
+
16
+ it 'leaves normal params untouched' do
17
+ mock_app do |env|
18
+ request = Rack::Request.new(env)
19
+ [200, {'Content-Type' => 'text/plain'}, [request.params['foo']]]
20
+ end
21
+ get '/', :foo => "bar"
22
+ body.should == 'bar'
23
+ end
24
+
25
+ it 'copes with nested arrays' do
26
+ mock_app do |env|
27
+ request = Rack::Request.new(env)
28
+ [200, {'Content-Type' => 'text/plain'}, [request.params['foo']['bar']]]
29
+ end
30
+ get '/', :foo => {:bar => "<bar>"}
31
+ body.should == '&lt;bar&gt;'
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,33 @@
1
+ require File.expand_path('../spec_helper.rb', __FILE__)
2
+
3
+ describe Rack::Protection::FormToken do
4
+ it_behaves_like "any rack application"
5
+
6
+ it "denies post requests without any token" do
7
+ post('/').should_not be_ok
8
+ end
9
+
10
+ it "accepts post requests with correct X-CSRF-Token header" do
11
+ post('/', {}, 'rack.session' => {:csrf => "a"}, 'HTTP_X_CSRF_TOKEN' => "a")
12
+ last_response.should be_ok
13
+ end
14
+
15
+ it "denies post requests with wrong X-CSRF-Token header" do
16
+ post('/', {}, 'rack.session' => {:csrf => "a"}, 'HTTP_X_CSRF_TOKEN' => "b")
17
+ last_response.should_not be_ok
18
+ end
19
+
20
+ it "accepts post form requests with correct authenticity_token field" do
21
+ post('/', {"authenticity_token" => "a"}, 'rack.session' => {:csrf => "a"})
22
+ last_response.should be_ok
23
+ end
24
+
25
+ it "denies post form requests with wrong authenticity_token field" do
26
+ post('/', {"authenticity_token" => "a"}, 'rack.session' => {:csrf => "b"})
27
+ last_response.should_not be_ok
28
+ end
29
+
30
+ it "accepts ajax requests without a valid token" do
31
+ post('/', {}, "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest").should be_ok
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ require File.expand_path('../spec_helper.rb', __FILE__)
2
+
3
+ describe Rack::Protection::FrameOptions do
4
+ it_behaves_like "any rack application"
5
+
6
+ it 'should set the X-XSS-Protection' do
7
+ get('/').headers["X-Frame-Options"].should == "sameorigin"
8
+ end
9
+
10
+ it 'should allow changing the protection mode' do
11
+ # I have no clue what other modes are available
12
+ mock_app do
13
+ use Rack::Protection::FrameOptions, :frame_options => :deny
14
+ run DummyApp
15
+ end
16
+
17
+ get('/').headers["X-Frame-Options"].should == "deny"
18
+ end
19
+
20
+ it 'should not override the header if already set' do
21
+ mock_app with_headers("X-Frame-Options" => "allow")
22
+ get('/').headers["X-Frame-Options"].should == "allow"
23
+ end
24
+ end
@@ -0,0 +1,35 @@
1
+ require File.expand_path('../spec_helper.rb', __FILE__)
2
+
3
+ describe Rack::Protection::IPSpoofing do
4
+ it_behaves_like "any rack application"
5
+
6
+ it 'accepts requests without X-Forward-For header' do
7
+ get('/', {}, 'HTTP_CLIENT_IP' => '1.2.3.4', 'HTTP_X_REAL_IP' => '4.3.2.1')
8
+ last_response.should be_ok
9
+ end
10
+
11
+ it 'accepts requests with proper X-Forward-For header' do
12
+ get('/', {}, 'HTTP_CLIENT_IP' => '1.2.3.4',
13
+ 'HTTP_X_FORWARDED_FOR' => '192.168.1.20, 1.2.3.4, 127.0.0.1')
14
+ last_response.should be_ok
15
+ end
16
+
17
+ it 'denies requests where the client spoofs X-Forward-For but not the IP' do
18
+ get('/', {}, 'HTTP_CLIENT_IP' => '1.2.3.4', 'HTTP_X_FORWARDED_FOR' => '1.2.3.5')
19
+ last_response.should_not be_ok
20
+ end
21
+
22
+ it 'denies requests where the client spoofs the IP but not X-Forward-For' do
23
+ get('/', {}, 'HTTP_CLIENT_IP' => '1.2.3.5',
24
+ 'HTTP_X_FORWARDED_FOR' => '192.168.1.20, 1.2.3.4, 127.0.0.1')
25
+ last_response.should_not be_ok
26
+ end
27
+
28
+ it 'denies requests where IP and X-Forward-For are spoofed but not X-Real-IP' do
29
+ get('/', {},
30
+ 'HTTP_CLIENT_IP' => '1.2.3.5',
31
+ 'HTTP_X_FORWARDED_FOR' => '1.2.3.5',
32
+ 'HTTP_X_REAL_IP' => '1.2.3.4')
33
+ last_response.should_not be_ok
34
+ end
35
+ end
@@ -0,0 +1,23 @@
1
+ require File.expand_path('../spec_helper.rb', __FILE__)
2
+
3
+ describe Rack::Protection::JsonCsrf do
4
+ it_behaves_like "any rack application"
5
+
6
+ describe 'json response' do
7
+ before do
8
+ mock_app { |e| [200, {'Content-Type' => 'application/json'}, []]}
9
+ end
10
+
11
+ it "denies get requests with json responses with a remote referrer" do
12
+ get('/', {}, 'HTTP_REFERER' => 'http://evil.com').should_not be_ok
13
+ end
14
+
15
+ it "accepts get requests with json responses with a local referrer" do
16
+ get('/', {}, 'HTTP_REFERER' => '/').should be_ok
17
+ end
18
+
19
+ it "accepts get requests with json responses with no referrer" do
20
+ get('/', {}).should be_ok
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ require File.expand_path('../spec_helper.rb', __FILE__)
2
+
3
+ describe Rack::Protection::PathTraversal do
4
+ it_behaves_like "any rack application"
5
+
6
+ context 'escaping' do
7
+ before do
8
+ mock_app { |e| [200, {'Content-Type' => 'text/plain'}, [e['PATH_INFO']]] }
9
+ end
10
+
11
+ %w[/foo/bar /foo/bar/ / /.f /a.x].each do |path|
12
+ it("does not touch #{path.inspect}") { get(path).body.should == path }
13
+ end
14
+
15
+ { # yes, this is ugly, feel free to change that
16
+ '/..' => '/', '/a/../b' => '/b', '/a/../b/' => '/b/', '/a/.' => '/a/',
17
+ '/%2e.' => '/', '/a/%2e%2e/b' => '/b', '/a%2f%2e%2e%2fb/' => '/b/',
18
+ '//' => '/', '/%2fetc%2fpasswd' => '/etc/passwd'
19
+ }.each do |a, b|
20
+ it("replaces #{a.inspect} with #{b.inspect}") { get(a).body.should == b }
21
+ end
22
+ end
23
+ end