rack-attack 5.0.1 → 5.4.2

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 (63) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +190 -94
  3. data/Rakefile +11 -4
  4. data/bin/setup +8 -0
  5. data/lib/rack/attack.rb +83 -51
  6. data/lib/rack/attack/allow2ban.rb +2 -1
  7. data/lib/rack/attack/blocklist.rb +0 -1
  8. data/lib/rack/attack/cache.rb +24 -5
  9. data/lib/rack/attack/check.rb +6 -8
  10. data/lib/rack/attack/fail2ban.rb +2 -1
  11. data/lib/rack/attack/path_normalizer.rb +6 -11
  12. data/lib/rack/attack/safelist.rb +0 -1
  13. data/lib/rack/attack/store_proxy.rb +3 -12
  14. data/lib/rack/attack/store_proxy/dalli_proxy.rb +2 -3
  15. data/lib/rack/attack/store_proxy/mem_cache_proxy.rb +4 -5
  16. data/lib/rack/attack/store_proxy/mem_cache_store_proxy.rb +19 -0
  17. data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +35 -0
  18. data/lib/rack/attack/store_proxy/redis_proxy.rb +54 -0
  19. data/lib/rack/attack/store_proxy/redis_store_proxy.rb +5 -24
  20. data/lib/rack/attack/throttle.rb +16 -12
  21. data/lib/rack/attack/track.rb +3 -3
  22. data/lib/rack/attack/version.rb +1 -1
  23. data/spec/acceptance/allow2ban_spec.rb +71 -0
  24. data/spec/acceptance/blocking_ip_spec.rb +38 -0
  25. data/spec/acceptance/blocking_spec.rb +41 -0
  26. data/spec/acceptance/blocking_subnet_spec.rb +44 -0
  27. data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +126 -0
  28. data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +121 -0
  29. data/spec/acceptance/cache_store_config_for_throttle_spec.rb +48 -0
  30. data/spec/acceptance/cache_store_config_with_rails_spec.rb +31 -0
  31. data/spec/acceptance/customizing_blocked_response_spec.rb +41 -0
  32. data/spec/acceptance/customizing_throttled_response_spec.rb +59 -0
  33. data/spec/acceptance/extending_request_object_spec.rb +34 -0
  34. data/spec/acceptance/fail2ban_spec.rb +76 -0
  35. data/spec/acceptance/safelisting_ip_spec.rb +48 -0
  36. data/spec/acceptance/safelisting_spec.rb +53 -0
  37. data/spec/acceptance/safelisting_subnet_spec.rb +48 -0
  38. data/spec/acceptance/stores/active_support_dalli_store_spec.rb +19 -0
  39. data/spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb +22 -0
  40. data/spec/acceptance/stores/active_support_mem_cache_store_spec.rb +18 -0
  41. data/spec/acceptance/stores/active_support_memory_store_spec.rb +16 -0
  42. data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +18 -0
  43. data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +18 -0
  44. data/spec/acceptance/stores/active_support_redis_store_spec.rb +18 -0
  45. data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +22 -0
  46. data/spec/acceptance/stores/dalli_client_spec.rb +19 -0
  47. data/spec/acceptance/stores/redis_spec.rb +20 -0
  48. data/spec/acceptance/stores/redis_store_spec.rb +18 -0
  49. data/spec/acceptance/throttling_spec.rb +159 -0
  50. data/spec/acceptance/track_spec.rb +27 -0
  51. data/spec/acceptance/track_throttle_spec.rb +53 -0
  52. data/spec/allow2ban_spec.rb +9 -8
  53. data/spec/fail2ban_spec.rb +11 -9
  54. data/spec/integration/offline_spec.rb +21 -23
  55. data/spec/rack_attack_dalli_proxy_spec.rb +0 -2
  56. data/spec/rack_attack_request_spec.rb +1 -1
  57. data/spec/rack_attack_spec.rb +13 -14
  58. data/spec/rack_attack_throttle_spec.rb +28 -18
  59. data/spec/rack_attack_track_spec.rb +11 -8
  60. data/spec/spec_helper.rb +35 -14
  61. data/spec/support/cache_store_helper.rb +82 -0
  62. metadata +150 -65
  63. data/spec/integration/rack_attack_cache_spec.rb +0 -122
@@ -1,10 +1,8 @@
1
1
  require_relative 'spec_helper'
2
2
 
3
3
  describe Rack::Attack::StoreProxy::DalliProxy do
4
-
5
4
  it 'should stub Dalli::Client#with on older clients' do
6
5
  proxy = Rack::Attack::StoreProxy::DalliProxy.new(Class.new)
7
6
  proxy.with {} # will not raise an error
8
7
  end
9
-
10
8
  end
@@ -14,6 +14,6 @@ describe 'Rack::Attack' do
14
14
  end
15
15
  end
16
16
 
17
- allow_ok_requests
17
+ it_allows_ok_requests
18
18
  end
19
19
  end
@@ -1,11 +1,11 @@
1
1
  require_relative 'spec_helper'
2
2
 
3
3
  describe 'Rack::Attack' do
4
- allow_ok_requests
4
+ it_allows_ok_requests
5
5
 
6
6
  describe 'normalizing paths' do
7
7
  before do
8
- Rack::Attack.blocklist("banned_path") {|req| req.path == '/foo' }
8
+ Rack::Attack.blocklist("banned_path") { |req| req.path == '/foo' }
9
9
  end
10
10
 
11
11
  it 'blocks requests with trailing slash' do
@@ -17,7 +17,7 @@ describe 'Rack::Attack' do
17
17
  describe 'blocklist' do
18
18
  before do
19
19
  @bad_ip = '1.2.3.4'
20
- Rack::Attack.blocklist("ip #{@bad_ip}") {|req| req.ip == @bad_ip }
20
+ Rack::Attack.blocklist("ip #{@bad_ip}") { |req| req.ip == @bad_ip }
21
21
  end
22
22
 
23
23
  it('has a blocklist') {
@@ -25,7 +25,7 @@ describe 'Rack::Attack' do
25
25
  }
26
26
 
27
27
  it('has a blacklist with a deprication warning') {
28
- _, stderror = capture_io do
28
+ _, stderror = capture_io do
29
29
  Rack::Attack.blacklists.key?("ip #{@bad_ip}").must_equal true
30
30
  end
31
31
  assert_match "[DEPRECATION] 'Rack::Attack.blacklists' is deprecated. Please use 'blocklists' instead.", stderror
@@ -33,29 +33,30 @@ describe 'Rack::Attack' do
33
33
 
34
34
  describe "a bad request" do
35
35
  before { get '/', {}, 'REMOTE_ADDR' => @bad_ip }
36
+
36
37
  it "should return a blocklist response" do
37
- get '/', {}, 'REMOTE_ADDR' => @bad_ip
38
38
  last_response.status.must_equal 403
39
39
  last_response.body.must_equal "Forbidden\n"
40
40
  end
41
+
41
42
  it "should tag the env" do
42
43
  last_request.env['rack.attack.matched'].must_equal "ip #{@bad_ip}"
43
44
  last_request.env['rack.attack.match_type'].must_equal :blocklist
44
45
  end
45
46
 
46
- allow_ok_requests
47
+ it_allows_ok_requests
47
48
  end
48
49
 
49
50
  describe "and safelist" do
50
51
  before do
51
52
  @good_ua = 'GoodUA'
52
- Rack::Attack.safelist("good ua") {|req| req.user_agent == @good_ua }
53
+ Rack::Attack.safelist("good ua") { |req| req.user_agent == @good_ua }
53
54
  end
54
55
 
55
- it('has a safelist'){ Rack::Attack.safelists.key?("good ua") }
56
+ it('has a safelist') { Rack::Attack.safelists.key?("good ua") }
56
57
 
57
58
  it('has a whitelist with a deprication warning') {
58
- _, stderror = capture_io do
59
+ _, stderror = capture_io do
59
60
  Rack::Attack.whitelists.key?("good ua")
60
61
  end
61
62
  assert_match "[DEPRECATION] 'Rack::Attack.whitelists' is deprecated. Please use 'safelists' instead.", stderror
@@ -63,10 +64,11 @@ describe 'Rack::Attack' do
63
64
 
64
65
  describe "with a request match both safelist & blocklist" do
65
66
  before { get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua }
67
+
66
68
  it "should allow safelists before blocklists" do
67
- get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua
68
69
  last_response.status.must_equal 200
69
70
  end
71
+
70
72
  it "should tag the env" do
71
73
  last_request.env['rack.attack.matched'].must_equal 'good ua'
72
74
  last_request.env['rack.attack.match_type'].must_equal :safelist
@@ -80,11 +82,10 @@ describe 'Rack::Attack' do
80
82
  end
81
83
 
82
84
  it 'should give a deprication warning for blacklisted_response' do
83
- _, stderror = capture_io do
85
+ _, stderror = capture_io do
84
86
  Rack::Attack.blacklisted_response
85
87
  end
86
88
  assert_match "[DEPRECATION] 'Rack::Attack.blacklisted_response' is deprecated. Please use 'blocklisted_response' instead.", stderror
87
-
88
89
  end
89
90
  end
90
91
 
@@ -93,7 +94,5 @@ describe 'Rack::Attack' do
93
94
  Rack::Attack.throttled_response.must_respond_to :call
94
95
  end
95
96
  end
96
-
97
97
  end
98
-
99
98
  end
@@ -1,4 +1,5 @@
1
1
  require_relative 'spec_helper'
2
+
2
3
  describe 'Rack::Attack.throttle' do
3
4
  before do
4
5
  @period = 60 # Use a long period; failures due to cache key rotation less likely
@@ -6,34 +7,40 @@ describe 'Rack::Attack.throttle' do
6
7
  Rack::Attack.throttle('ip/sec', :limit => 1, :period => @period) { |req| req.ip }
7
8
  end
8
9
 
9
- it('should have a throttle'){ Rack::Attack.throttles.key?('ip/sec') }
10
- allow_ok_requests
10
+ it('should have a throttle') { Rack::Attack.throttles.key?('ip/sec') }
11
+
12
+ it_allows_ok_requests
11
13
 
12
14
  describe 'a single request' do
13
15
  before { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
16
+
14
17
  it 'should set the counter for one request' do
15
- key = "rack::attack:#{Time.now.to_i/@period}:ip/sec:1.2.3.4"
18
+ key = "rack::attack:#{Time.now.to_i / @period}:ip/sec:1.2.3.4"
16
19
  Rack::Attack.cache.store.read(key).must_equal 1
17
20
  end
18
21
 
19
22
  it 'should populate throttle data' do
20
- data = { :count => 1, :limit => 1, :period => @period }
23
+ data = { :count => 1, :limit => 1, :period => @period, epoch_time: Rack::Attack.cache.last_epoch_time.to_i }
21
24
  last_request.env['rack.attack.throttle_data']['ip/sec'].must_equal data
22
25
  end
23
26
  end
27
+
24
28
  describe "with 2 requests" do
25
29
  before do
26
30
  2.times { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
27
31
  end
32
+
28
33
  it 'should block the last request' do
29
34
  last_response.status.must_equal 429
30
35
  end
36
+
31
37
  it 'should tag the env' do
32
38
  last_request.env['rack.attack.matched'].must_equal 'ip/sec'
33
39
  last_request.env['rack.attack.match_type'].must_equal :throttle
34
- last_request.env['rack.attack.match_data'].must_equal({:count => 2, :limit => 1, :period => @period})
40
+ last_request.env['rack.attack.match_data'].must_equal(:count => 2, :limit => 1, :period => @period, epoch_time: Rack::Attack.cache.last_epoch_time.to_i)
35
41
  last_request.env['rack.attack.match_discriminator'].must_equal('1.2.3.4')
36
42
  end
43
+
37
44
  it 'should set a Retry-After header' do
38
45
  last_response.headers['Retry-After'].must_equal @period.to_s
39
46
  end
@@ -44,20 +51,21 @@ describe 'Rack::Attack.throttle with limit as proc' do
44
51
  before do
45
52
  @period = 60 # Use a long period; failures due to cache key rotation less likely
46
53
  Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
47
- Rack::Attack.throttle('ip/sec', :limit => lambda { |req| 1 }, :period => @period) { |req| req.ip }
54
+ Rack::Attack.throttle('ip/sec', :limit => lambda { |_req| 1 }, :period => @period) { |req| req.ip }
48
55
  end
49
56
 
50
- allow_ok_requests
57
+ it_allows_ok_requests
51
58
 
52
59
  describe 'a single request' do
53
60
  before { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
61
+
54
62
  it 'should set the counter for one request' do
55
- key = "rack::attack:#{Time.now.to_i/@period}:ip/sec:1.2.3.4"
63
+ key = "rack::attack:#{Time.now.to_i / @period}:ip/sec:1.2.3.4"
56
64
  Rack::Attack.cache.store.read(key).must_equal 1
57
65
  end
58
66
 
59
67
  it 'should populate throttle data' do
60
- data = { :count => 1, :limit => 1, :period => @period }
68
+ data = { :count => 1, :limit => 1, :period => @period, epoch_time: Rack::Attack.cache.last_epoch_time.to_i }
61
69
  last_request.env['rack.attack.throttle_data']['ip/sec'].must_equal data
62
70
  end
63
71
  end
@@ -67,20 +75,21 @@ describe 'Rack::Attack.throttle with period as proc' do
67
75
  before do
68
76
  @period = 60 # Use a long period; failures due to cache key rotation less likely
69
77
  Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
70
- Rack::Attack.throttle('ip/sec', :limit => lambda { |req| 1 }, :period => lambda { |req| @period }) { |req| req.ip }
78
+ Rack::Attack.throttle('ip/sec', :limit => lambda { |_req| 1 }, :period => lambda { |_req| @period }) { |req| req.ip }
71
79
  end
72
80
 
73
- allow_ok_requests
81
+ it_allows_ok_requests
74
82
 
75
83
  describe 'a single request' do
76
84
  before { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
85
+
77
86
  it 'should set the counter for one request' do
78
- key = "rack::attack:#{Time.now.to_i/@period}:ip/sec:1.2.3.4"
87
+ key = "rack::attack:#{Time.now.to_i / @period}:ip/sec:1.2.3.4"
79
88
  Rack::Attack.cache.store.read(key).must_equal 1
80
89
  end
81
90
 
82
91
  it 'should populate throttle data' do
83
- data = { :count => 1, :limit => 1, :period => @period }
92
+ data = { :count => 1, :limit => 1, :period => @period, epoch_time: Rack::Attack.cache.last_epoch_time.to_i }
84
93
  last_request.env['rack.attack.throttle_data']['ip/sec'].must_equal data
85
94
  end
86
95
  end
@@ -93,17 +102,18 @@ describe 'Rack::Attack.throttle with block retuning nil' do
93
102
  Rack::Attack.throttle('ip/sec', :limit => 1, :period => @period) { |_| nil }
94
103
  end
95
104
 
96
- allow_ok_requests
105
+ it_allows_ok_requests
97
106
 
98
107
  describe 'a single request' do
99
108
  before { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
109
+
100
110
  it 'should not set the counter' do
101
- key = "rack::attack:#{Time.now.to_i/@period}:ip/sec:1.2.3.4"
102
- Rack::Attack.cache.store.read(key).must_equal nil
111
+ key = "rack::attack:#{Time.now.to_i / @period}:ip/sec:1.2.3.4"
112
+ assert_nil Rack::Attack.cache.store.read(key)
103
113
  end
104
114
 
105
115
  it 'should not populate throttle data' do
106
- last_request.env['rack.attack.throttle_data'].must_equal nil
116
+ assert_nil last_request.env['rack.attack.throttle_data']
107
117
  end
108
118
  end
109
- end
119
+ end
@@ -16,9 +16,11 @@ describe 'Rack::Attack.track' do
16
16
  end
17
17
 
18
18
  before do
19
- Rack::Attack.track("everything"){ |req| true }
19
+ Rack::Attack.track("everything") { |_req| true }
20
20
  end
21
- allow_ok_requests
21
+
22
+ it_allows_ok_requests
23
+
22
24
  it "should tag the env" do
23
25
  get '/'
24
26
  last_request.env['rack.attack.matched'].must_equal 'everything'
@@ -29,11 +31,12 @@ describe 'Rack::Attack.track' do
29
31
  before do
30
32
  Counter.reset
31
33
  # A second track
32
- Rack::Attack.track("homepage"){ |req| req.path == "/"}
34
+ Rack::Attack.track("homepage") { |req| req.path == "/" }
33
35
 
34
- ActiveSupport::Notifications.subscribe("rack.attack") do |*args|
36
+ ActiveSupport::Notifications.subscribe("rack.attack") do |*_args|
35
37
  Counter.incr
36
38
  end
39
+
37
40
  get "/"
38
41
  end
39
42
 
@@ -44,15 +47,15 @@ describe 'Rack::Attack.track' do
44
47
 
45
48
  describe "without limit and period options" do
46
49
  it "should assign the track filter to a Check instance" do
47
- tracker = Rack::Attack.track("homepage") { |req| req.path == "/"}
48
- tracker.filter.class.must_equal Rack::Attack::Check
50
+ track = Rack::Attack.track("homepage") { |req| req.path == "/" }
51
+ track.filter.class.must_equal Rack::Attack::Check
49
52
  end
50
53
  end
51
54
 
52
55
  describe "with limit and period options" do
53
56
  it "should assign the track filter to a Throttle instance" do
54
- tracker = Rack::Attack.track("homepage", :limit => 10, :period => 10) { |req| req.path == "/"}
55
- tracker.filter.class.must_equal Rack::Attack::Throttle
57
+ track = Rack::Attack.track("homepage", :limit => 10, :period => 10) { |req| req.path == "/" }
58
+ track.filter.class.must_equal Rack::Attack::Throttle
56
59
  end
57
60
  end
58
61
  end
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,3 @@
1
- require "rubygems"
2
1
  require "bundler/setup"
3
2
 
4
3
  require "minitest/autorun"
@@ -7,31 +6,53 @@ require "rack/test"
7
6
  require 'active_support'
8
7
  require 'action_dispatch'
9
8
 
10
- # Load Journey for Rails 3.2
11
- require 'journey' if ActionPack::VERSION::MAJOR == 3
12
-
13
9
  require "rack/attack"
14
10
 
15
- begin
16
- require 'pry'
17
- rescue LoadError
18
- #nothing to do here
11
+ if RUBY_ENGINE == "ruby"
12
+ require "byebug"
19
13
  end
20
14
 
21
- class MiniTest::Spec
15
+ def safe_require(name)
16
+ begin
17
+ require name
18
+ rescue LoadError
19
+ end
20
+ end
21
+
22
+ safe_require "connection_pool"
23
+ safe_require "dalli"
24
+ safe_require "redis"
25
+ safe_require "redis-activesupport"
26
+ safe_require "redis-store"
22
27
 
28
+ class MiniTest::Spec
23
29
  include Rack::Test::Methods
24
30
 
25
- after { Rack::Attack.clear! }
31
+ before do
32
+ @_original_throttled_response = Rack::Attack.throttled_response
33
+ @_original_blocklisted_response = Rack::Attack.blocklisted_response
34
+ end
35
+
36
+ after do
37
+ Rack::Attack.clear_configuration
38
+ Rack::Attack.instance_variable_set(:@cache, nil)
39
+
40
+ Rack::Attack.throttled_response = @_original_throttled_response
41
+ Rack::Attack.blocklisted_response = @_original_blocklisted_response
42
+ end
26
43
 
27
44
  def app
28
- Rack::Builder.new {
45
+ Rack::Builder.new do
46
+ # Use Rack::Lint to test that rack-attack is complying with the rack spec
47
+ use Rack::Lint
29
48
  use Rack::Attack
30
- run lambda {|env| [200, {}, ['Hello World']]}
31
- }.to_app
49
+ use Rack::Lint
50
+
51
+ run lambda { |_env| [200, {}, ['Hello World']] }
52
+ end.to_app
32
53
  end
33
54
 
34
- def self.allow_ok_requests
55
+ def self.it_allows_ok_requests
35
56
  it "must allow ok requests" do
36
57
  get '/', {}, 'REMOTE_ADDR' => '127.0.0.1'
37
58
  last_response.status.must_equal 200
@@ -0,0 +1,82 @@
1
+ class Minitest::Spec
2
+ def self.it_works_for_cache_backed_features(options)
3
+ fetch_from_store = options.fetch(:fetch_from_store)
4
+
5
+ it "works for throttle" do
6
+ Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
7
+ request.ip
8
+ end
9
+
10
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
11
+ assert_equal 200, last_response.status
12
+
13
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
14
+ assert_equal 429, last_response.status
15
+ end
16
+
17
+ it "works for fail2ban" do
18
+ Rack::Attack.blocklist("fail2ban pentesters") do |request|
19
+ Rack::Attack::Fail2Ban.filter(request.ip, maxretry: 2, findtime: 30, bantime: 60) do
20
+ request.path.include?("private-place")
21
+ end
22
+ end
23
+
24
+ get "/"
25
+ assert_equal 200, last_response.status
26
+
27
+ get "/private-place"
28
+ assert_equal 403, last_response.status
29
+
30
+ get "/private-place"
31
+ assert_equal 403, last_response.status
32
+
33
+ get "/"
34
+ assert_equal 403, last_response.status
35
+ end
36
+
37
+ it "works for allow2ban" do
38
+ Rack::Attack.blocklist("allow2ban pentesters") do |request|
39
+ Rack::Attack::Allow2Ban.filter(request.ip, maxretry: 2, findtime: 30, bantime: 60) do
40
+ request.path.include?("scarce-resource")
41
+ end
42
+ end
43
+
44
+ get "/"
45
+ assert_equal 200, last_response.status
46
+
47
+ get "/scarce-resource"
48
+ assert_equal 200, last_response.status
49
+
50
+ get "/scarce-resource"
51
+ assert_equal 200, last_response.status
52
+
53
+ get "/scarce-resource"
54
+ assert_equal 403, last_response.status
55
+
56
+ get "/"
57
+ assert_equal 403, last_response.status
58
+ end
59
+
60
+ it "doesn't leak keys" do
61
+ Rack::Attack.throttle("by ip", limit: 1, period: 1) do |request|
62
+ request.ip
63
+ end
64
+
65
+ key = nil
66
+
67
+ # Freeze time during these statement to be sure that the key used by rack attack is the same
68
+ # we pre-calculate in local variable `key`
69
+ Timecop.freeze do
70
+ key = "rack::attack:#{Time.now.to_i}:by ip:1.2.3.4"
71
+
72
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
73
+ end
74
+
75
+ assert fetch_from_store.call(key)
76
+
77
+ sleep 2.1
78
+
79
+ assert_nil fetch_from_store.call(key)
80
+ end
81
+ end
82
+ end