rack-protection-monkey 1.5.3

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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/License +20 -0
  3. data/README.md +90 -0
  4. data/Rakefile +48 -0
  5. data/lib/rack-protection.rb +1 -0
  6. data/lib/rack/protection.rb +40 -0
  7. data/lib/rack/protection/authenticity_token.rb +31 -0
  8. data/lib/rack/protection/base.rb +121 -0
  9. data/lib/rack/protection/escaped_params.rb +87 -0
  10. data/lib/rack/protection/form_token.rb +23 -0
  11. data/lib/rack/protection/frame_options.rb +37 -0
  12. data/lib/rack/protection/http_origin.rb +34 -0
  13. data/lib/rack/protection/ip_spoofing.rb +23 -0
  14. data/lib/rack/protection/json_csrf.rb +35 -0
  15. data/lib/rack/protection/path_traversal.rb +47 -0
  16. data/lib/rack/protection/remote_referrer.rb +20 -0
  17. data/lib/rack/protection/remote_token.rb +22 -0
  18. data/lib/rack/protection/session_hijacking.rb +36 -0
  19. data/lib/rack/protection/version.rb +16 -0
  20. data/lib/rack/protection/xss_header.rb +25 -0
  21. data/rack-protection.gemspec +123 -0
  22. data/spec/lib/rack/protection/authenticity_token_spec.rb +46 -0
  23. data/spec/lib/rack/protection/base_spec.rb +38 -0
  24. data/spec/lib/rack/protection/escaped_params_spec.rb +41 -0
  25. data/spec/lib/rack/protection/form_token_spec.rb +31 -0
  26. data/spec/lib/rack/protection/frame_options_spec.rb +37 -0
  27. data/spec/lib/rack/protection/http_origin_spec.rb +40 -0
  28. data/spec/lib/rack/protection/ip_spoofing_spec.rb +33 -0
  29. data/spec/lib/rack/protection/json_csrf_spec.rb +56 -0
  30. data/spec/lib/rack/protection/path_traversal_spec.rb +39 -0
  31. data/spec/lib/rack/protection/protection_spec.rb +103 -0
  32. data/spec/lib/rack/protection/remote_referrer_spec.rb +29 -0
  33. data/spec/lib/rack/protection/remote_token_spec.rb +40 -0
  34. data/spec/lib/rack/protection/session_hijacking_spec.rb +53 -0
  35. data/spec/lib/rack/protection/xss_header_spec.rb +54 -0
  36. data/spec/spec_helper.rb +86 -0
  37. data/spec/support/dummy_app.rb +7 -0
  38. data/spec/support/not_implemented_as_pending.rb +23 -0
  39. data/spec/support/rack_monkey_patches.rb +21 -0
  40. data/spec/support/shared_examples.rb +65 -0
  41. data/spec/support/spec_helpers.rb +36 -0
  42. metadata +180 -0
@@ -0,0 +1,46 @@
1
+ describe Rack::Protection::AuthenticityToken do
2
+ it_behaves_like "any rack application"
3
+
4
+ it "denies post requests without any token" do
5
+ expect(post('/')).not_to be_ok
6
+ end
7
+
8
+ it "accepts post requests with correct X-CSRF-Token header" do
9
+ post('/', {}, 'rack.session' => {:csrf => "a"}, 'HTTP_X_CSRF_TOKEN' => "a")
10
+ expect(last_response).to be_ok
11
+ end
12
+
13
+ it "denies post requests with wrong X-CSRF-Token header" do
14
+ post('/', {}, 'rack.session' => {:csrf => "a"}, 'HTTP_X_CSRF_TOKEN' => "b")
15
+ expect(last_response).not_to be_ok
16
+ end
17
+
18
+ it "accepts post form requests with correct authenticity_token field" do
19
+ post('/', {"authenticity_token" => "a"}, 'rack.session' => {:csrf => "a"})
20
+ expect(last_response).to be_ok
21
+ end
22
+
23
+ it "denies post form requests with wrong authenticity_token field" do
24
+ post('/', {"authenticity_token" => "a"}, 'rack.session' => {:csrf => "b"})
25
+ expect(last_response).not_to be_ok
26
+ end
27
+
28
+ it "prevents ajax requests without a valid token" do
29
+ expect(post('/', {}, "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest")).not_to be_ok
30
+ end
31
+
32
+ it "allows for a custom authenticity token param" do
33
+ mock_app do
34
+ use Rack::Protection::AuthenticityToken, :authenticity_param => 'csrf_param'
35
+ run proc { |e| [200, {'Content-Type' => 'text/plain'}, ['hi']] }
36
+ end
37
+
38
+ post('/', {"csrf_param" => "a"}, 'rack.session' => {:csrf => "a"})
39
+ expect(last_response).to be_ok
40
+ end
41
+
42
+ it "sets a new csrf token for the session in env, even after a 'safe' request" do
43
+ get('/', {}, {})
44
+ expect(env['rack.session'][:csrf]).not_to be_nil
45
+ end
46
+ end
@@ -0,0 +1,38 @@
1
+ describe Rack::Protection::Base do
2
+
3
+ subject { described_class.new(lambda {}) }
4
+
5
+ describe "#random_string" do
6
+ it "outputs a string of 32 characters" do
7
+ expect(subject.random_string.length).to eq(32)
8
+ end
9
+ end
10
+
11
+ describe "#referrer" do
12
+ it "Reads referrer from Referer header" do
13
+ env = {"HTTP_HOST" => "foo.com", "HTTP_REFERER" => "http://bar.com/valid"}
14
+ expect(subject.referrer(env)).to eq("bar.com")
15
+ end
16
+
17
+ it "Reads referrer from Host header when Referer header is relative" do
18
+ env = {"HTTP_HOST" => "foo.com", "HTTP_REFERER" => "/valid"}
19
+ expect(subject.referrer(env)).to eq("foo.com")
20
+ end
21
+
22
+ it "Reads referrer from Host header when Referer header is missing" do
23
+ env = {"HTTP_HOST" => "foo.com"}
24
+ expect(subject.referrer(env)).to eq("foo.com")
25
+ end
26
+
27
+ it "Returns nil when Referer header is missing and allow_empty_referrer is false" do
28
+ env = {"HTTP_HOST" => "foo.com"}
29
+ subject.options[:allow_empty_referrer] = false
30
+ expect(subject.referrer(env)).to be_nil
31
+ end
32
+
33
+ it "Returns nil when Referer header is invalid" do
34
+ env = {"HTTP_HOST" => "foo.com", "HTTP_REFERER" => "http://bar.com/bad|uri"}
35
+ expect(subject.referrer(env)).to be_nil
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,41 @@
1
+ describe Rack::Protection::EscapedParams do
2
+ it_behaves_like "any rack application"
3
+
4
+ context 'escaping' do
5
+ it 'escapes html entities' do
6
+ mock_app do |env|
7
+ request = Rack::Request.new(env)
8
+ [200, {'Content-Type' => 'text/plain'}, [request.params['foo']]]
9
+ end
10
+ get '/', :foo => "<bar>"
11
+ expect(body).to eq('&lt;bar&gt;')
12
+ end
13
+
14
+ it 'leaves normal params untouched' do
15
+ mock_app do |env|
16
+ request = Rack::Request.new(env)
17
+ [200, {'Content-Type' => 'text/plain'}, [request.params['foo']]]
18
+ end
19
+ get '/', :foo => "bar"
20
+ expect(body).to eq('bar')
21
+ end
22
+
23
+ it 'copes with nested arrays' do
24
+ mock_app do |env|
25
+ request = Rack::Request.new(env)
26
+ [200, {'Content-Type' => 'text/plain'}, [request.params['foo']['bar']]]
27
+ end
28
+ get '/', :foo => {:bar => "<bar>"}
29
+ expect(body).to eq('&lt;bar&gt;')
30
+ end
31
+
32
+ it 'leaves cache-breaker params untouched' do
33
+ mock_app do |env|
34
+ [200, {'Content-Type' => 'text/plain'}, ['hi']]
35
+ end
36
+
37
+ get '/?95df8d9bf5237ad08df3115ee74dcb10'
38
+ expect(body).to eq('hi')
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,31 @@
1
+ describe Rack::Protection::FormToken do
2
+ it_behaves_like "any rack application"
3
+
4
+ it "denies post requests without any token" do
5
+ expect(post('/')).not_to be_ok
6
+ end
7
+
8
+ it "accepts post requests with correct X-CSRF-Token header" do
9
+ post('/', {}, 'rack.session' => {:csrf => "a"}, 'HTTP_X_CSRF_TOKEN' => "a")
10
+ expect(last_response).to be_ok
11
+ end
12
+
13
+ it "denies post requests with wrong X-CSRF-Token header" do
14
+ post('/', {}, 'rack.session' => {:csrf => "a"}, 'HTTP_X_CSRF_TOKEN' => "b")
15
+ expect(last_response).not_to be_ok
16
+ end
17
+
18
+ it "accepts post form requests with correct authenticity_token field" do
19
+ post('/', {"authenticity_token" => "a"}, 'rack.session' => {:csrf => "a"})
20
+ expect(last_response).to be_ok
21
+ end
22
+
23
+ it "denies post form requests with wrong authenticity_token field" do
24
+ post('/', {"authenticity_token" => "a"}, 'rack.session' => {:csrf => "b"})
25
+ expect(last_response).not_to be_ok
26
+ end
27
+
28
+ it "accepts ajax requests without a valid token" do
29
+ expect(post('/', {}, "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest")).to be_ok
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ describe Rack::Protection::FrameOptions do
2
+ it_behaves_like "any rack application"
3
+
4
+ it 'should set the X-Frame-Options' do
5
+ expect(get('/', {}, 'wants' => 'text/html').headers["X-Frame-Options"]).to eq("SAMEORIGIN")
6
+ end
7
+
8
+ it 'should not set the X-Frame-Options for other content types' do
9
+ expect(get('/', {}, 'wants' => 'text/foo').headers["X-Frame-Options"]).to be_nil
10
+ end
11
+
12
+ it 'should allow changing the protection mode' do
13
+ # I have no clue what other modes are available
14
+ mock_app do
15
+ use Rack::Protection::FrameOptions, :frame_options => :deny
16
+ run DummyApp
17
+ end
18
+
19
+ expect(get('/', {}, 'wants' => 'text/html').headers["X-Frame-Options"]).to eq("DENY")
20
+ end
21
+
22
+
23
+ it 'should allow changing the protection mode to a string' do
24
+ # I have no clue what other modes are available
25
+ mock_app do
26
+ use Rack::Protection::FrameOptions, :frame_options => "ALLOW-FROM foo"
27
+ run DummyApp
28
+ end
29
+
30
+ expect(get('/', {}, 'wants' => 'text/html').headers["X-Frame-Options"]).to eq("ALLOW-FROM foo")
31
+ end
32
+
33
+ it 'should not override the header if already set' do
34
+ mock_app with_headers("X-Frame-Options" => "allow")
35
+ expect(get('/', {}, 'wants' => 'text/html').headers["X-Frame-Options"]).to eq("allow")
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ describe Rack::Protection::HttpOrigin do
2
+ it_behaves_like "any rack application"
3
+
4
+ before(:each) do
5
+ mock_app do
6
+ use Rack::Protection::HttpOrigin
7
+ run DummyApp
8
+ end
9
+ end
10
+
11
+ %w(GET HEAD POST PUT DELETE).each do |method|
12
+ it "accepts #{method} requests with no Origin" do
13
+ expect(send(method.downcase, '/')).to be_ok
14
+ end
15
+ end
16
+
17
+ %w(GET HEAD).each do |method|
18
+ it "accepts #{method} requests with non-whitelisted Origin" do
19
+ expect(send(method.downcase, '/', {}, 'HTTP_ORIGIN' => 'http://malicious.com')).to be_ok
20
+ end
21
+ end
22
+
23
+ %w(POST PUT DELETE).each do |method|
24
+ it "denies #{method} requests with non-whitelisted Origin" do
25
+ expect(send(method.downcase, '/', {}, 'HTTP_ORIGIN' => 'http://malicious.com')).not_to be_ok
26
+ end
27
+
28
+ it "accepts #{} requests with 'null' Origin" do
29
+ expect(send(method.downcase, '/', {}, 'HTTP_ORIGIN' => 'null')).to be_ok
30
+ end
31
+
32
+ it "accepts #{method} requests with whitelisted Origin" do
33
+ mock_app do
34
+ use Rack::Protection::HttpOrigin, :origin_whitelist => ['http://www.friend.com']
35
+ run DummyApp
36
+ end
37
+ expect(send(method.downcase, '/', {}, 'HTTP_ORIGIN' => 'http://www.friend.com')).to be_ok
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,33 @@
1
+ describe Rack::Protection::IPSpoofing do
2
+ it_behaves_like "any rack application"
3
+
4
+ it 'accepts requests without X-Forward-For header' do
5
+ get('/', {}, 'HTTP_CLIENT_IP' => '1.2.3.4', 'HTTP_X_REAL_IP' => '4.3.2.1')
6
+ expect(last_response).to be_ok
7
+ end
8
+
9
+ it 'accepts requests with proper X-Forward-For header' do
10
+ get('/', {}, 'HTTP_CLIENT_IP' => '1.2.3.4',
11
+ 'HTTP_X_FORWARDED_FOR' => '192.168.1.20, 1.2.3.4, 127.0.0.1')
12
+ expect(last_response).to be_ok
13
+ end
14
+
15
+ it 'denies requests where the client spoofs X-Forward-For but not the IP' do
16
+ get('/', {}, 'HTTP_CLIENT_IP' => '1.2.3.4', 'HTTP_X_FORWARDED_FOR' => '1.2.3.5')
17
+ expect(last_response).not_to be_ok
18
+ end
19
+
20
+ it 'denies requests where the client spoofs the IP but not X-Forward-For' do
21
+ get('/', {}, 'HTTP_CLIENT_IP' => '1.2.3.5',
22
+ 'HTTP_X_FORWARDED_FOR' => '192.168.1.20, 1.2.3.4, 127.0.0.1')
23
+ expect(last_response).not_to be_ok
24
+ end
25
+
26
+ it 'denies requests where IP and X-Forward-For are spoofed but not X-Real-IP' do
27
+ get('/', {},
28
+ 'HTTP_CLIENT_IP' => '1.2.3.5',
29
+ 'HTTP_X_FORWARDED_FOR' => '1.2.3.5',
30
+ 'HTTP_X_REAL_IP' => '1.2.3.4')
31
+ expect(last_response).not_to be_ok
32
+ end
33
+ end
@@ -0,0 +1,56 @@
1
+ describe Rack::Protection::JsonCsrf do
2
+ it_behaves_like "any rack application"
3
+
4
+ describe 'json response' do
5
+ before do
6
+ mock_app { |e| [200, {'Content-Type' => 'application/json'}, []]}
7
+ end
8
+
9
+ it "denies get requests with json responses with a remote referrer" do
10
+ expect(get('/', {}, 'HTTP_REFERER' => 'http://evil.com')).not_to be_ok
11
+ end
12
+
13
+ it "accepts requests with json responses with a remote referrer when there's an origin header set" do
14
+ expect(get('/', {}, 'HTTP_REFERER' => 'http://good.com', 'HTTP_ORIGIN' => 'http://good.com')).to be_ok
15
+ end
16
+
17
+ it "accepts requests with json responses with a remote referrer when there's an x-origin header set" do
18
+ expect(get('/', {}, 'HTTP_REFERER' => 'http://good.com', 'HTTP_X_ORIGIN' => 'http://good.com')).to be_ok
19
+ end
20
+
21
+ it "accepts get requests with json responses with a local referrer" do
22
+ expect(get('/', {}, 'HTTP_REFERER' => '/')).to be_ok
23
+ end
24
+
25
+ it "accepts get requests with json responses with no referrer" do
26
+ expect(get('/', {})).to be_ok
27
+ end
28
+
29
+ it "accepts XHR requests" do
30
+ expect(get('/', {}, 'HTTP_REFERER' => 'http://evil.com', 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest')).to be_ok
31
+ end
32
+
33
+ end
34
+
35
+ describe 'not json response' do
36
+
37
+ it "accepts get requests with 304 headers" do
38
+ mock_app { |e| [304, {}, []]}
39
+ expect(get('/', {}).status).to eq(304)
40
+ end
41
+
42
+ end
43
+
44
+ describe 'with drop_session as default reaction' do
45
+ it 'still denies' do
46
+ mock_app do
47
+ use Rack::Protection, :reaction => :drop_session
48
+ run proc { |e| [200, {'Content-Type' => 'application/json'}, []]}
49
+ end
50
+
51
+ session = {:foo => :bar}
52
+ get('/', {}, 'HTTP_REFERER' => 'http://evil.com', 'rack.session' => session)
53
+ expect(last_response).not_to be_ok
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,39 @@
1
+ describe Rack::Protection::PathTraversal do
2
+ it_behaves_like "any rack application"
3
+
4
+ context 'escaping' do
5
+ before do
6
+ mock_app { |e| [200, {'Content-Type' => 'text/plain'}, [e['PATH_INFO']]] }
7
+ end
8
+
9
+ %w[/foo/bar /foo/bar/ / /.f /a.x].each do |path|
10
+ it("does not touch #{path.inspect}") { expect(get(path).body).to eq(path) }
11
+ end
12
+
13
+ { # yes, this is ugly, feel free to change that
14
+ '/..' => '/', '/a/../b' => '/b', '/a/../b/' => '/b/', '/a/.' => '/a/',
15
+ '/%2e.' => '/', '/a/%2E%2e/b' => '/b', '/a%2f%2E%2e%2Fb/' => '/b/',
16
+ '//' => '/', '/%2fetc%2Fpasswd' => '/etc/passwd'
17
+ }.each do |a, b|
18
+ it("replaces #{a.inspect} with #{b.inspect}") { expect(get(a).body).to eq(b) }
19
+ end
20
+
21
+ it 'should be able to deal with PATH_INFO = nil (fcgi?)' do
22
+ app = Rack::Protection::PathTraversal.new(proc { 42 })
23
+ expect(app.call({})).to eq(42)
24
+ end
25
+ end
26
+
27
+ if "".respond_to?(:encoding) # Ruby 1.9+ M17N
28
+ context "PATH_INFO's encoding" do
29
+ before do
30
+ @app = Rack::Protection::PathTraversal.new(proc { |e| [200, {'Content-Type' => 'text/plain'}, [e['PATH_INFO'].encoding.to_s]] })
31
+ end
32
+
33
+ it 'should remain unchanged as ASCII-8BIT' do
34
+ body = @app.call({ 'PATH_INFO' => '/'.encode('ASCII-8BIT') })[2][0]
35
+ expect(body).to eq('ASCII-8BIT')
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,103 @@
1
+ describe Rack::Protection do
2
+ it_behaves_like "any rack application"
3
+
4
+ it 'passes on options' do
5
+ mock_app do
6
+ use Rack::Protection, :track => ['HTTP_FOO']
7
+ run proc { |e| [200, {'Content-Type' => 'text/plain'}, ['hi']] }
8
+ end
9
+
10
+ session = {:foo => :bar}
11
+ get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_ENCODING' => 'a'
12
+ get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_ENCODING' => 'b'
13
+ expect(session[:foo]).to eq(:bar)
14
+
15
+ get '/', {}, 'rack.session' => session, 'HTTP_FOO' => 'BAR'
16
+ expect(session).to be_empty
17
+ end
18
+
19
+ it 'passes errors through if :reaction => :report is used' do
20
+ mock_app do
21
+ use Rack::Protection, :reaction => :report
22
+ run proc { |e| [200, {'Content-Type' => 'text/plain'}, [e["protection.failed"].to_s]] }
23
+ end
24
+
25
+ session = {:foo => :bar}
26
+ post('/', {}, 'rack.session' => session, 'HTTP_ORIGIN' => 'http://malicious.com')
27
+ expect(last_response).to be_ok
28
+ expect(body).to eq("true")
29
+ end
30
+
31
+ describe "#react" do
32
+ it 'prevents attacks and warns about it' do
33
+ io = StringIO.new
34
+ mock_app do
35
+ use Rack::Protection, :logger => Logger.new(io)
36
+ run DummyApp
37
+ end
38
+ post('/', {}, 'rack.session' => {}, 'HTTP_ORIGIN' => 'http://malicious.com')
39
+ expect(io.string).to match(/prevented.*Origin/)
40
+ end
41
+
42
+ it 'reports attacks if reaction is to report' do
43
+ io = StringIO.new
44
+ mock_app do
45
+ use Rack::Protection, :reaction => :report, :logger => Logger.new(io)
46
+ run DummyApp
47
+ end
48
+ post('/', {}, 'rack.session' => {}, 'HTTP_ORIGIN' => 'http://malicious.com')
49
+ expect(io.string).to match(/reported.*Origin/)
50
+ expect(io.string).not_to match(/prevented.*Origin/)
51
+ end
52
+
53
+ it 'passes errors to reaction method if specified' do
54
+ io = StringIO.new
55
+ Rack::Protection::Base.send(:define_method, :special) { |*args| io << args.inspect }
56
+ mock_app do
57
+ use Rack::Protection, :reaction => :special, :logger => Logger.new(io)
58
+ run DummyApp
59
+ end
60
+ post('/', {}, 'rack.session' => {}, 'HTTP_ORIGIN' => 'http://malicious.com')
61
+ expect(io.string).to match(/HTTP_ORIGIN.*malicious.com/)
62
+ expect(io.string).not_to match(/reported|prevented/)
63
+ end
64
+ end
65
+
66
+ describe "#html?" do
67
+ context "given an appropriate content-type header" do
68
+ subject { Rack::Protection::Base.new(nil).html? 'content-type' => "text/html" }
69
+ it { is_expected.to be_truthy }
70
+ end
71
+
72
+ context "given an inappropriate content-type header" do
73
+ subject { Rack::Protection::Base.new(nil).html? 'content-type' => "image/gif" }
74
+ it { is_expected.to be_falsey }
75
+ end
76
+
77
+ context "given no content-type header" do
78
+ subject { Rack::Protection::Base.new(nil).html?({}) }
79
+ it { is_expected.to be_falsey }
80
+ end
81
+ end
82
+
83
+ describe "#instrument" do
84
+ let(:env) { { 'rack.protection.attack' => 'base' } }
85
+ let(:instrumenter) { double('Instrumenter') }
86
+
87
+ after do
88
+ app.instrument(env)
89
+ end
90
+
91
+ context 'with an instrumenter specified' do
92
+ let(:app) { Rack::Protection::Base.new(nil, :instrumenter => instrumenter) }
93
+
94
+ it { expect(instrumenter).to receive(:instrument).with('rack.protection', env) }
95
+ end
96
+
97
+ context 'with no instrumenter specified' do
98
+ let(:app) { Rack::Protection::Base.new(nil) }
99
+
100
+ it { expect(instrumenter).not_to receive(:instrument) }
101
+ end
102
+ end
103
+ end