rack-protection 0.1.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.

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