rack-attack 5.1.0 → 5.2.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.
@@ -20,6 +20,9 @@ module Rack
20
20
  end
21
21
 
22
22
  def read(unprefixed_key)
23
+ enforce_store_presence!
24
+ enforce_store_method_presence!(:read)
25
+
23
26
  store.read("#{prefix}:#{unprefixed_key}")
24
27
  end
25
28
 
@@ -46,18 +49,29 @@ module Rack
46
49
  end
47
50
 
48
51
  def do_count(key, expires_in)
52
+ enforce_store_presence!
53
+ enforce_store_method_presence!(:increment)
54
+
55
+ result = store.increment(key, 1, :expires_in => expires_in)
56
+
57
+ # NB: Some stores return nil when incrementing uninitialized values
58
+ if result.nil?
59
+ enforce_store_method_presence!(:write)
60
+
61
+ store.write(key, 1, :expires_in => expires_in)
62
+ end
63
+ result || 1
64
+ end
65
+
66
+ def enforce_store_presence!
49
67
  if store.nil?
50
68
  raise Rack::Attack::MissingStoreError
51
- elsif !store.respond_to?(:increment)
52
- raise Rack::Attack::MisconfiguredStoreError, "Store needs to respond to #increment"
53
- else
54
- result = store.increment(key, 1, :expires_in => expires_in)
55
-
56
- # NB: Some stores return nil when incrementing uninitialized values
57
- if result.nil?
58
- store.write(key, 1, :expires_in => expires_in)
59
- end
60
- result || 1
69
+ end
70
+ end
71
+
72
+ def enforce_store_method_presence!(method_name)
73
+ if !store.respond_to?(method_name)
74
+ raise Rack::Attack::MisconfiguredStoreError, "Store needs to respond to ##{method_name}"
61
75
  end
62
76
  end
63
77
  end
@@ -17,6 +17,7 @@ module Rack
17
17
  }
18
18
  end
19
19
 
20
+ alias_method :match?, :[]
20
21
  end
21
22
  end
22
23
  end
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  class Attack
3
- VERSION = '5.1.0'
3
+ VERSION = '5.2.0'
4
4
  end
5
5
  end
@@ -0,0 +1,71 @@
1
+ require_relative "../spec_helper"
2
+ require "timecop"
3
+
4
+ describe "allow2ban" do
5
+ before do
6
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
7
+
8
+ Rack::Attack.blocklist("allow2ban pentesters") do |request|
9
+ Rack::Attack::Allow2Ban.filter(request.ip, maxretry: 2, findtime: 30, bantime: 60) do
10
+ request.path.include?("scarce-resource")
11
+ end
12
+ end
13
+ end
14
+
15
+ it "returns OK for many requests that doesn't match the filter" do
16
+ get "/"
17
+ assert_equal 200, last_response.status
18
+
19
+ get "/"
20
+ assert_equal 200, last_response.status
21
+ end
22
+
23
+ it "returns OK for first request that matches the filter" do
24
+ get "/scarce-resource"
25
+ assert_equal 200, last_response.status
26
+ end
27
+
28
+ it "forbids all access after reaching maxretry limit" do
29
+ get "/scarce-resource"
30
+ assert_equal 200, last_response.status
31
+
32
+ get "/scarce-resource"
33
+ assert_equal 200, last_response.status
34
+
35
+ get "/scarce-resource"
36
+ assert_equal 403, last_response.status
37
+
38
+ get "/"
39
+ assert_equal 403, last_response.status
40
+ end
41
+
42
+ it "restores access after bantime elapsed" do
43
+ get "/scarce-resource"
44
+ assert_equal 200, last_response.status
45
+
46
+ get "/scarce-resource"
47
+ assert_equal 200, last_response.status
48
+
49
+ get "/"
50
+ assert_equal 403, last_response.status
51
+
52
+ Timecop.travel(60) do
53
+ get "/"
54
+
55
+ assert_equal 200, last_response.status
56
+ end
57
+ end
58
+
59
+ it "does not forbid all access if maxrety condition is met but not within the findtime timespan" do
60
+ get "/scarce-resource"
61
+ assert_equal 200, last_response.status
62
+
63
+ Timecop.travel(31) do
64
+ get "/scarce-resource"
65
+ assert_equal 200, last_response.status
66
+
67
+ get "/"
68
+ assert_equal 200, last_response.status
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,38 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "Blocking an IP" do
4
+ before do
5
+ Rack::Attack.blocklist_ip("1.2.3.4")
6
+ end
7
+
8
+ it "forbids request if IP matches" do
9
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
10
+
11
+ assert_equal 403, last_response.status
12
+ end
13
+
14
+ it "succeeds if IP doesn't match" do
15
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
16
+
17
+ assert_equal 200, last_response.status
18
+ end
19
+
20
+ it "notifies when the request is blocked" do
21
+ notified = false
22
+ notification_type = nil
23
+
24
+ ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
25
+ notified = true
26
+ notification_type = request.env["rack.attack.match_type"]
27
+ end
28
+
29
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
30
+
31
+ refute notified
32
+
33
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
34
+
35
+ assert notified
36
+ assert_equal :blocklist, notification_type
37
+ end
38
+ end
@@ -18,4 +18,24 @@ describe "#blocklist" do
18
18
 
19
19
  assert_equal 200, last_response.status
20
20
  end
21
+
22
+ it "notifies when the request is blocked" do
23
+ notification_matched = nil
24
+ notification_type = nil
25
+
26
+ ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
27
+ notification_matched = request.env["rack.attack.matched"]
28
+ notification_type = request.env["rack.attack.match_type"]
29
+ end
30
+
31
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
32
+
33
+ assert_nil notification_matched
34
+ assert_nil notification_type
35
+
36
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
37
+
38
+ assert_equal "block 1.2.3.4", notification_matched
39
+ assert_equal :blocklist, notification_type
40
+ end
21
41
  end
@@ -0,0 +1,44 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "Blocking an IP subnet" do
4
+ before do
5
+ Rack::Attack.blocklist_ip("1.2.3.4/31")
6
+ end
7
+
8
+ it "forbids request if IP is inside the subnet" do
9
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
10
+
11
+ assert_equal 403, last_response.status
12
+ end
13
+
14
+ it "forbids request for another IP in the subnet" do
15
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.5"
16
+
17
+ assert_equal 403, last_response.status
18
+ end
19
+
20
+ it "succeeds if IP is outside the subnet" do
21
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.6"
22
+
23
+ assert_equal 200, last_response.status
24
+ end
25
+
26
+ it "notifies when the request is blocked" do
27
+ notified = false
28
+ notification_type = nil
29
+
30
+ ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
31
+ notified = true
32
+ notification_type = request.env["rack.attack.match_type"]
33
+ end
34
+
35
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
36
+
37
+ refute notified
38
+
39
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
40
+
41
+ assert notified
42
+ assert_equal :blocklist, notification_type
43
+ end
44
+ end
@@ -0,0 +1,111 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "Cache store config when using allow2ban" do
4
+ before do
5
+ Rack::Attack.blocklist("allow2ban pentesters") do |request|
6
+ Rack::Attack::Allow2Ban.filter(request.ip, maxretry: 2, findtime: 30, bantime: 60) do
7
+ request.path.include?("scarce-resource")
8
+ end
9
+ end
10
+ end
11
+
12
+ it "gives semantic error if no store was configured" do
13
+ assert_raises(Rack::Attack::MissingStoreError) do
14
+ get "/scarce-resource"
15
+ end
16
+ end
17
+
18
+ it "gives semantic error if store is missing #read method" do
19
+ basic_store_class = Class.new do
20
+ def write(key, value)
21
+ end
22
+
23
+ def increment(key, count, options = {})
24
+ end
25
+ end
26
+
27
+ Rack::Attack.cache.store = basic_store_class.new
28
+
29
+ raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
30
+ get "/scarce-resource"
31
+ end
32
+
33
+ assert_equal "Store needs to respond to #read", raised_exception.message
34
+ end
35
+
36
+ it "gives semantic error if store is missing #write method" do
37
+ basic_store_class = Class.new do
38
+ def read(key)
39
+ end
40
+
41
+ def increment(key, count, options = {})
42
+ end
43
+ end
44
+
45
+ Rack::Attack.cache.store = basic_store_class.new
46
+
47
+ raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
48
+ get "/scarce-resource"
49
+ end
50
+
51
+ assert_equal "Store needs to respond to #write", raised_exception.message
52
+ end
53
+
54
+ it "gives semantic error if store is missing #increment method" do
55
+ basic_store_class = Class.new do
56
+ def read(key)
57
+ end
58
+
59
+ def write(key, value)
60
+ end
61
+ end
62
+
63
+ Rack::Attack.cache.store = basic_store_class.new
64
+
65
+ raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
66
+ get "/scarce-resource"
67
+ end
68
+
69
+ assert_equal "Store needs to respond to #increment", raised_exception.message
70
+ end
71
+
72
+ it "works with any object that responds to #read, #write and #increment" do
73
+ basic_store_class = Class.new do
74
+ attr_accessor :backend
75
+
76
+ def initialize
77
+ @backend = {}
78
+ end
79
+
80
+ def read(key)
81
+ @backend[key]
82
+ end
83
+
84
+ def write(key, value, options = {})
85
+ @backend[key] = value
86
+ end
87
+
88
+ def increment(key, count, options = {})
89
+ @backend[key] ||= 0
90
+ @backend[key] += 1
91
+ end
92
+ end
93
+
94
+ Rack::Attack.cache.store = basic_store_class.new
95
+
96
+ get "/"
97
+ assert_equal 200, last_response.status
98
+
99
+ get "/scarce-resource"
100
+ assert_equal 200, last_response.status
101
+
102
+ get "/scarce-resource"
103
+ assert_equal 200, last_response.status
104
+
105
+ get "/scarce-resource"
106
+ assert_equal 403, last_response.status
107
+
108
+ get "/"
109
+ assert_equal 403, last_response.status
110
+ end
111
+ end
@@ -0,0 +1,108 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "Cache store config when using fail2ban" do
4
+ before do
5
+ Rack::Attack.blocklist("fail2ban pentesters") do |request|
6
+ Rack::Attack::Fail2Ban.filter(request.ip, maxretry: 2, findtime: 30, bantime: 60) do
7
+ request.path.include?("private-place")
8
+ end
9
+ end
10
+ end
11
+
12
+ it "gives semantic error if no store was configured" do
13
+ assert_raises(Rack::Attack::MissingStoreError) do
14
+ get "/private-place"
15
+ end
16
+ end
17
+
18
+ it "gives semantic error if store is missing #read method" do
19
+ basic_store_class = Class.new do
20
+ def write(key, value)
21
+ end
22
+
23
+ def increment(key, count, options = {})
24
+ end
25
+ end
26
+
27
+ Rack::Attack.cache.store = basic_store_class.new
28
+
29
+ raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
30
+ get "/private-place"
31
+ end
32
+
33
+ assert_equal "Store needs to respond to #read", raised_exception.message
34
+ end
35
+
36
+ it "gives semantic error if store is missing #write method" do
37
+ basic_store_class = Class.new do
38
+ def read(key)
39
+ end
40
+
41
+ def increment(key, count, options = {})
42
+ end
43
+ end
44
+
45
+ Rack::Attack.cache.store = basic_store_class.new
46
+
47
+ raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
48
+ get "/private-place"
49
+ end
50
+
51
+ assert_equal "Store needs to respond to #write", raised_exception.message
52
+ end
53
+
54
+ it "gives semantic error if store is missing #increment method" do
55
+ basic_store_class = Class.new do
56
+ def read(key)
57
+ end
58
+
59
+ def write(key, value)
60
+ end
61
+ end
62
+
63
+ Rack::Attack.cache.store = basic_store_class.new
64
+
65
+ raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
66
+ get "/private-place"
67
+ end
68
+
69
+ assert_equal "Store needs to respond to #increment", raised_exception.message
70
+ end
71
+
72
+ it "works with any object that responds to #read, #write and #increment" do
73
+ basic_store_class = Class.new do
74
+ attr_accessor :backend
75
+
76
+ def initialize
77
+ @backend = {}
78
+ end
79
+
80
+ def read(key)
81
+ @backend[key]
82
+ end
83
+
84
+ def write(key, value, options = {})
85
+ @backend[key] = value
86
+ end
87
+
88
+ def increment(key, count, options = {})
89
+ @backend[key] ||= 0
90
+ @backend[key] += 1
91
+ end
92
+ end
93
+
94
+ Rack::Attack.cache.store = basic_store_class.new
95
+
96
+ get "/"
97
+ assert_equal 200, last_response.status
98
+
99
+ get "/private-place"
100
+ assert_equal 403, last_response.status
101
+
102
+ get "/private-place"
103
+ assert_equal 403, last_response.status
104
+
105
+ get "/"
106
+ assert_equal 403, last_response.status
107
+ end
108
+ end