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.
- 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
|