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.
- data/License +20 -0
- data/README.md +80 -0
- data/Rakefile +37 -0
- data/lib/rack-protection.rb +1 -0
- data/lib/rack/protection.rb +35 -0
- data/lib/rack/protection/authenticity_token.rb +24 -0
- data/lib/rack/protection/base.rb +97 -0
- data/lib/rack/protection/escaped_params.rb +61 -0
- data/lib/rack/protection/form_token.rb +23 -0
- data/lib/rack/protection/frame_options.rb +26 -0
- data/lib/rack/protection/ip_spoofing.rb +23 -0
- data/lib/rack/protection/json_csrf.rb +25 -0
- data/lib/rack/protection/path_traversal.rb +29 -0
- data/lib/rack/protection/remote_referrer.rb +23 -0
- data/lib/rack/protection/remote_token.rb +22 -0
- data/lib/rack/protection/session_hijacking.rb +36 -0
- data/lib/rack/protection/version.rb +44 -0
- data/lib/rack/protection/xss_header.rb +27 -0
- data/rack-protection.gemspec +61 -0
- data/spec/authenticity_token_spec.rb +33 -0
- data/spec/escaped_params_spec.rb +34 -0
- data/spec/form_token_spec.rb +33 -0
- data/spec/frame_options_spec.rb +24 -0
- data/spec/ip_spoofing_spec.rb +35 -0
- data/spec/json_csrf_spec.rb +23 -0
- data/spec/path_traversal_spec.rb +23 -0
- data/spec/protection_spec.rb +5 -0
- data/spec/remote_referrer_spec.rb +31 -0
- data/spec/remote_token_spec.rb +42 -0
- data/spec/session_hijacking_spec.rb +40 -0
- data/spec/spec_helper.rb +157 -0
- data/spec/xss_header_spec.rb +24 -0
- metadata +121 -0
@@ -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 == '<bar>'
|
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 == '<bar>'
|
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
|