rack-protection 1.5.5 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +13 -0
  3. data/License +4 -1
  4. data/README.md +41 -13
  5. data/Rakefile +29 -5
  6. data/lib/rack/protection.rb +41 -24
  7. data/lib/rack/protection/authenticity_token.rb +181 -9
  8. data/lib/rack/protection/base.rb +3 -22
  9. data/lib/rack/protection/content_security_policy.rb +79 -0
  10. data/lib/rack/protection/cookie_tossing.rb +75 -0
  11. data/lib/rack/protection/escaped_params.rb +2 -0
  12. data/lib/rack/protection/form_token.rb +1 -1
  13. data/lib/rack/protection/http_origin.rb +17 -2
  14. data/lib/rack/protection/json_csrf.rb +26 -4
  15. data/lib/rack/protection/path_traversal.rb +4 -12
  16. data/lib/rack/protection/referrer_policy.rb +25 -0
  17. data/lib/rack/protection/remote_token.rb +1 -1
  18. data/lib/rack/protection/session_hijacking.rb +1 -1
  19. data/lib/rack/protection/strict_transport.rb +39 -0
  20. data/lib/rack/protection/version.rb +1 -12
  21. data/lib/rack/protection/xss_header.rb +1 -1
  22. data/rack-protection.gemspec +26 -104
  23. metadata +21 -82
  24. data/spec/authenticity_token_spec.rb +0 -48
  25. data/spec/base_spec.rb +0 -40
  26. data/spec/escaped_params_spec.rb +0 -43
  27. data/spec/form_token_spec.rb +0 -33
  28. data/spec/frame_options_spec.rb +0 -39
  29. data/spec/http_origin_spec.rb +0 -38
  30. data/spec/ip_spoofing_spec.rb +0 -35
  31. data/spec/json_csrf_spec.rb +0 -58
  32. data/spec/path_traversal_spec.rb +0 -41
  33. data/spec/protection_spec.rb +0 -105
  34. data/spec/remote_referrer_spec.rb +0 -31
  35. data/spec/remote_token_spec.rb +0 -42
  36. data/spec/session_hijacking_spec.rb +0 -55
  37. data/spec/spec_helper.rb +0 -163
  38. data/spec/xss_header_spec.rb +0 -56
@@ -1,33 +0,0 @@
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
@@ -1,39 +0,0 @@
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-Frame-Options' do
7
- get('/', {}, 'wants' => 'text/html').headers["X-Frame-Options"].should == "SAMEORIGIN"
8
- end
9
-
10
- it 'should not set the X-Frame-Options for other content types' do
11
- get('/', {}, 'wants' => 'text/foo').headers["X-Frame-Options"].should be_nil
12
- end
13
-
14
- it 'should allow changing the protection mode' do
15
- # I have no clue what other modes are available
16
- mock_app do
17
- use Rack::Protection::FrameOptions, :frame_options => :deny
18
- run DummyApp
19
- end
20
-
21
- get('/', {}, 'wants' => 'text/html').headers["X-Frame-Options"].should == "DENY"
22
- end
23
-
24
-
25
- it 'should allow changing the protection mode to a string' do
26
- # I have no clue what other modes are available
27
- mock_app do
28
- use Rack::Protection::FrameOptions, :frame_options => "ALLOW-FROM foo"
29
- run DummyApp
30
- end
31
-
32
- get('/', {}, 'wants' => 'text/html').headers["X-Frame-Options"].should == "ALLOW-FROM foo"
33
- end
34
-
35
- it 'should not override the header if already set' do
36
- mock_app with_headers("X-Frame-Options" => "allow")
37
- get('/', {}, 'wants' => 'text/html').headers["X-Frame-Options"].should == "allow"
38
- end
39
- end
@@ -1,38 +0,0 @@
1
- require File.expand_path('../spec_helper.rb', __FILE__)
2
-
3
- describe Rack::Protection::HttpOrigin do
4
- it_behaves_like "any rack application"
5
-
6
- before(:each) do
7
- mock_app do
8
- use Rack::Protection::HttpOrigin
9
- run DummyApp
10
- end
11
- end
12
-
13
- %w(GET HEAD POST PUT DELETE).each do |method|
14
- it "accepts #{method} requests with no Origin" do
15
- send(method.downcase, '/').should be_ok
16
- end
17
- end
18
-
19
- %w(GET HEAD).each do |method|
20
- it "accepts #{method} requests with non-whitelisted Origin" do
21
- send(method.downcase, '/', {}, 'HTTP_ORIGIN' => 'http://malicious.com').should be_ok
22
- end
23
- end
24
-
25
- %w(POST PUT DELETE).each do |method|
26
- it "denies #{method} requests with non-whitelisted Origin" do
27
- send(method.downcase, '/', {}, 'HTTP_ORIGIN' => 'http://malicious.com').should_not be_ok
28
- end
29
-
30
- it "accepts #{method} requests with whitelisted Origin" do
31
- mock_app do
32
- use Rack::Protection::HttpOrigin, :origin_whitelist => ['http://www.friend.com']
33
- run DummyApp
34
- end
35
- send(method.downcase, '/', {}, 'HTTP_ORIGIN' => 'http://www.friend.com').should be_ok
36
- end
37
- end
38
- end
@@ -1,35 +0,0 @@
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
@@ -1,58 +0,0 @@
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 requests with json responses with a remote referrer when there's an origin header set" do
16
- get('/', {}, 'HTTP_REFERER' => 'http://good.com', 'HTTP_ORIGIN' => 'http://good.com').should be_ok
17
- end
18
-
19
- it "accepts requests with json responses with a remote referrer when there's an x-origin header set" do
20
- get('/', {}, 'HTTP_REFERER' => 'http://good.com', 'HTTP_X_ORIGIN' => 'http://good.com').should be_ok
21
- end
22
-
23
- it "accepts get requests with json responses with a local referrer" do
24
- get('/', {}, 'HTTP_REFERER' => '/').should be_ok
25
- end
26
-
27
- it "accepts get requests with json responses with no referrer" do
28
- get('/', {}).should be_ok
29
- end
30
-
31
- it "accepts XHR requests" do
32
- get('/', {}, 'HTTP_REFERER' => 'http://evil.com', 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest').should be_ok
33
- end
34
-
35
- end
36
-
37
- describe 'not json response' do
38
-
39
- it "accepts get requests with 304 headers" do
40
- mock_app { |e| [304, {}, []]}
41
- get('/', {}).status.should == 304
42
- end
43
-
44
- end
45
-
46
- describe 'with drop_session as default reaction' do
47
- it 'still denies' do
48
- mock_app do
49
- use Rack::Protection, :reaction => :drop_session
50
- run proc { |e| [200, {'Content-Type' => 'application/json'}, []]}
51
- end
52
-
53
- session = {:foo => :bar}
54
- get('/', {}, 'HTTP_REFERER' => 'http://evil.com', 'rack.session' => session)
55
- last_response.should_not be_ok
56
- end
57
- end
58
- end
@@ -1,41 +0,0 @@
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
-
23
- it 'should be able to deal with PATH_INFO = nil (fcgi?)' do
24
- app = Rack::Protection::PathTraversal.new(proc { 42 })
25
- app.call({}).should be == 42
26
- end
27
- end
28
-
29
- if "".respond_to?(:encoding) # Ruby 1.9+ M17N
30
- context "PATH_INFO's encoding" do
31
- before do
32
- @app = Rack::Protection::PathTraversal.new(proc { |e| [200, {'Content-Type' => 'text/plain'}, [e['PATH_INFO'].encoding.to_s]] })
33
- end
34
-
35
- it 'should remain unchanged as ASCII-8BIT' do
36
- body = @app.call({ 'PATH_INFO' => '/'.encode('ASCII-8BIT') })[2][0]
37
- body.should == 'ASCII-8BIT'
38
- end
39
- end
40
- end
41
- end
@@ -1,105 +0,0 @@
1
- require File.expand_path('../spec_helper.rb', __FILE__)
2
-
3
- describe Rack::Protection do
4
- it_behaves_like "any rack application"
5
-
6
- it 'passes on options' do
7
- mock_app do
8
- use Rack::Protection, :track => ['HTTP_FOO']
9
- run proc { |e| [200, {'Content-Type' => 'text/plain'}, ['hi']] }
10
- end
11
-
12
- session = {:foo => :bar}
13
- get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_ENCODING' => 'a'
14
- get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_ENCODING' => 'b'
15
- session[:foo].should be == :bar
16
-
17
- get '/', {}, 'rack.session' => session, 'HTTP_FOO' => 'BAR'
18
- session.should be_empty
19
- end
20
-
21
- it 'passes errors through if :reaction => :report is used' do
22
- mock_app do
23
- use Rack::Protection, :reaction => :report
24
- run proc { |e| [200, {'Content-Type' => 'text/plain'}, [e["protection.failed"].to_s]] }
25
- end
26
-
27
- session = {:foo => :bar}
28
- post('/', {}, 'rack.session' => session, 'HTTP_ORIGIN' => 'http://malicious.com')
29
- last_response.should be_ok
30
- body.should == "true"
31
- end
32
-
33
- describe "#react" do
34
- it 'prevents attacks and warns about it' do
35
- io = StringIO.new
36
- mock_app do
37
- use Rack::Protection, :logger => Logger.new(io)
38
- run DummyApp
39
- end
40
- post('/', {}, 'rack.session' => {}, 'HTTP_ORIGIN' => 'http://malicious.com')
41
- io.string.should match /prevented.*Origin/
42
- end
43
-
44
- it 'reports attacks if reaction is to report' do
45
- io = StringIO.new
46
- mock_app do
47
- use Rack::Protection, :reaction => :report, :logger => Logger.new(io)
48
- run DummyApp
49
- end
50
- post('/', {}, 'rack.session' => {}, 'HTTP_ORIGIN' => 'http://malicious.com')
51
- io.string.should match /reported.*Origin/
52
- io.string.should_not match /prevented.*Origin/
53
- end
54
-
55
- it 'passes errors to reaction method if specified' do
56
- io = StringIO.new
57
- Rack::Protection::Base.send(:define_method, :special) { |*args| io << args.inspect }
58
- mock_app do
59
- use Rack::Protection, :reaction => :special, :logger => Logger.new(io)
60
- run DummyApp
61
- end
62
- post('/', {}, 'rack.session' => {}, 'HTTP_ORIGIN' => 'http://malicious.com')
63
- io.string.should match /HTTP_ORIGIN.*malicious.com/
64
- io.string.should_not match /reported|prevented/
65
- end
66
- end
67
-
68
- describe "#html?" do
69
- context "given an appropriate content-type header" do
70
- subject { Rack::Protection::Base.new(nil).html? 'content-type' => "text/html" }
71
- it { should be_true }
72
- end
73
-
74
- context "given an inappropriate content-type header" do
75
- subject { Rack::Protection::Base.new(nil).html? 'content-type' => "image/gif" }
76
- it { should be_false }
77
- end
78
-
79
- context "given no content-type header" do
80
- subject { Rack::Protection::Base.new(nil).html?({}) }
81
- it { should be_false }
82
- end
83
- end
84
-
85
- describe "#instrument" do
86
- let(:env) { { 'rack.protection.attack' => 'base' } }
87
- let(:instrumenter) { double('Instrumenter') }
88
-
89
- after do
90
- app.instrument(env)
91
- end
92
-
93
- context 'with an instrumenter specified' do
94
- let(:app) { Rack::Protection::Base.new(nil, :instrumenter => instrumenter) }
95
-
96
- it { instrumenter.should_receive(:instrument).with('rack.protection', env) }
97
- end
98
-
99
- context 'with no instrumenter specified' do
100
- let(:app) { Rack::Protection::Base.new(nil) }
101
-
102
- it { instrumenter.should_not_receive(:instrument) }
103
- end
104
- end
105
- end
@@ -1,31 +0,0 @@
1
- require File.expand_path('../spec_helper.rb', __FILE__)
2
-
3
- describe Rack::Protection::RemoteReferrer do
4
- it_behaves_like "any rack application"
5
-
6
- it "accepts post requests with no referrer" do
7
- post('/').should be_ok
8
- end
9
-
10
- it "does not accept post requests with no referrer if allow_empty_referrer is false" do
11
- mock_app do
12
- use Rack::Protection::RemoteReferrer, :allow_empty_referrer => false
13
- run DummyApp
14
- end
15
- post('/').should_not be_ok
16
- end
17
-
18
- it "should allow post request with a relative referrer" do
19
- post('/', {}, 'HTTP_REFERER' => '/').should be_ok
20
- end
21
-
22
- it "accepts post requests with the same host in the referrer" do
23
- post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.com')
24
- last_response.should be_ok
25
- end
26
-
27
- it "denies post requests with a remote referrer" do
28
- post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org')
29
- last_response.should_not be_ok
30
- end
31
- end
@@ -1,42 +0,0 @@
1
- require File.expand_path('../spec_helper.rb', __FILE__)
2
-
3
- describe Rack::Protection::RemoteToken do
4
- it_behaves_like "any rack application"
5
-
6
- it "accepts post requests with no referrer" do
7
- post('/').should be_ok
8
- end
9
-
10
- it "accepts post requests with a local referrer" do
11
- post('/', {}, 'HTTP_REFERER' => '/').should be_ok
12
- end
13
-
14
- it "denies post requests with a remote referrer and no token" do
15
- post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org')
16
- last_response.should_not be_ok
17
- end
18
-
19
- it "accepts post requests with a remote referrer and correct X-CSRF-Token header" do
20
- post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org',
21
- 'rack.session' => {:csrf => "a"}, 'HTTP_X_CSRF_TOKEN' => "a")
22
- last_response.should be_ok
23
- end
24
-
25
- it "denies post requests with a remote referrer and wrong X-CSRF-Token header" do
26
- post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org',
27
- 'rack.session' => {:csrf => "a"}, 'HTTP_X_CSRF_TOKEN' => "b")
28
- last_response.should_not be_ok
29
- end
30
-
31
- it "accepts post form requests with a remote referrer and correct authenticity_token field" do
32
- post('/', {"authenticity_token" => "a"}, 'HTTP_REFERER' => 'http://example.com/foo',
33
- 'HTTP_HOST' => 'example.org', 'rack.session' => {:csrf => "a"})
34
- last_response.should be_ok
35
- end
36
-
37
- it "denies post form requests with a remote referrer and wrong authenticity_token field" do
38
- post('/', {"authenticity_token" => "a"}, 'HTTP_REFERER' => 'http://example.com/foo',
39
- 'HTTP_HOST' => 'example.org', 'rack.session' => {:csrf => "b"})
40
- last_response.should_not be_ok
41
- end
42
- end
@@ -1,55 +0,0 @@
1
- require File.expand_path('../spec_helper.rb', __FILE__)
2
-
3
- describe Rack::Protection::SessionHijacking do
4
- it_behaves_like "any rack application"
5
-
6
- it "accepts a session without changes to tracked parameters" do
7
- session = {:foo => :bar}
8
- get '/', {}, 'rack.session' => session
9
- get '/', {}, 'rack.session' => session
10
- session[:foo].should == :bar
11
- end
12
-
13
- it "denies requests with a changing User-Agent header" do
14
- session = {:foo => :bar}
15
- get '/', {}, 'rack.session' => session, 'HTTP_USER_AGENT' => 'a'
16
- get '/', {}, 'rack.session' => session, 'HTTP_USER_AGENT' => 'b'
17
- session.should be_empty
18
- end
19
-
20
- it "accepts requests with a changing Accept-Encoding header" do
21
- # this is tested because previously it led to clearing the session
22
- session = {:foo => :bar}
23
- get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_ENCODING' => 'a'
24
- get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_ENCODING' => 'b'
25
- session.should_not be_empty
26
- end
27
-
28
- it "denies requests with a changing Accept-Language header" do
29
- session = {:foo => :bar}
30
- get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_LANGUAGE' => 'a'
31
- get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_LANGUAGE' => 'b'
32
- session.should be_empty
33
- end
34
-
35
- it "accepts requests with the same Accept-Language header" do
36
- session = {:foo => :bar}
37
- get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_LANGUAGE' => 'a'
38
- get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_LANGUAGE' => 'a'
39
- session.should_not be_empty
40
- end
41
-
42
- it "comparison of Accept-Language header is not case sensitive" do
43
- session = {:foo => :bar}
44
- get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_LANGUAGE' => 'a'
45
- get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_LANGUAGE' => 'A'
46
- session.should_not be_empty
47
- end
48
-
49
- it "accepts requests with a changing Version header"do
50
- session = {:foo => :bar}
51
- get '/', {}, 'rack.session' => session, 'HTTP_VERSION' => '1.0'
52
- get '/', {}, 'rack.session' => session, 'HTTP_VERSION' => '1.1'
53
- session[:foo].should == :bar
54
- end
55
- end