rack-attack 4.3.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 (64) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +230 -113
  3. data/Rakefile +11 -3
  4. data/bin/setup +8 -0
  5. data/lib/rack/attack.rb +121 -48
  6. data/lib/rack/attack/allow2ban.rb +2 -1
  7. data/lib/rack/attack/{whitelist.rb → blocklist.rb} +2 -3
  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 +3 -2
  11. data/lib/rack/attack/path_normalizer.rb +6 -11
  12. data/lib/rack/attack/request.rb +1 -1
  13. data/lib/rack/attack/{blacklist.rb → safelist.rb} +2 -4
  14. data/lib/rack/attack/store_proxy.rb +13 -12
  15. data/lib/rack/attack/store_proxy/dalli_proxy.rb +2 -3
  16. data/lib/rack/attack/store_proxy/mem_cache_proxy.rb +50 -0
  17. data/lib/rack/attack/store_proxy/mem_cache_store_proxy.rb +19 -0
  18. data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +35 -0
  19. data/lib/rack/attack/store_proxy/redis_proxy.rb +54 -0
  20. data/lib/rack/attack/store_proxy/redis_store_proxy.rb +5 -24
  21. data/lib/rack/attack/throttle.rb +16 -12
  22. data/lib/rack/attack/track.rb +3 -3
  23. data/lib/rack/attack/version.rb +1 -1
  24. data/spec/acceptance/allow2ban_spec.rb +71 -0
  25. data/spec/acceptance/blocking_ip_spec.rb +38 -0
  26. data/spec/acceptance/blocking_spec.rb +41 -0
  27. data/spec/acceptance/blocking_subnet_spec.rb +44 -0
  28. data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +126 -0
  29. data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +121 -0
  30. data/spec/acceptance/cache_store_config_for_throttle_spec.rb +48 -0
  31. data/spec/acceptance/cache_store_config_with_rails_spec.rb +31 -0
  32. data/spec/acceptance/customizing_blocked_response_spec.rb +41 -0
  33. data/spec/acceptance/customizing_throttled_response_spec.rb +59 -0
  34. data/spec/acceptance/extending_request_object_spec.rb +34 -0
  35. data/spec/acceptance/fail2ban_spec.rb +76 -0
  36. data/spec/acceptance/safelisting_ip_spec.rb +48 -0
  37. data/spec/acceptance/safelisting_spec.rb +53 -0
  38. data/spec/acceptance/safelisting_subnet_spec.rb +48 -0
  39. data/spec/acceptance/stores/active_support_dalli_store_spec.rb +19 -0
  40. data/spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb +22 -0
  41. data/spec/acceptance/stores/active_support_mem_cache_store_spec.rb +18 -0
  42. data/spec/acceptance/stores/active_support_memory_store_spec.rb +16 -0
  43. data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +18 -0
  44. data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +18 -0
  45. data/spec/acceptance/stores/active_support_redis_store_spec.rb +18 -0
  46. data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +22 -0
  47. data/spec/acceptance/stores/dalli_client_spec.rb +19 -0
  48. data/spec/acceptance/stores/redis_spec.rb +20 -0
  49. data/spec/acceptance/stores/redis_store_spec.rb +18 -0
  50. data/spec/acceptance/throttling_spec.rb +159 -0
  51. data/spec/acceptance/track_spec.rb +27 -0
  52. data/spec/acceptance/track_throttle_spec.rb +53 -0
  53. data/spec/allow2ban_spec.rb +10 -9
  54. data/spec/fail2ban_spec.rb +12 -10
  55. data/spec/integration/offline_spec.rb +21 -23
  56. data/spec/rack_attack_dalli_proxy_spec.rb +0 -2
  57. data/spec/rack_attack_request_spec.rb +2 -2
  58. data/spec/rack_attack_spec.rb +53 -18
  59. data/spec/rack_attack_throttle_spec.rb +45 -13
  60. data/spec/rack_attack_track_spec.rb +11 -8
  61. data/spec/spec_helper.rb +35 -14
  62. data/spec/support/cache_store_helper.rb +82 -0
  63. metadata +161 -61
  64. data/spec/integration/rack_attack_cache_spec.rb +0 -119
@@ -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
@@ -0,0 +1,41 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "#blocklist" do
4
+ before do
5
+ Rack::Attack.blocklist("block 1.2.3.4") do |request|
6
+ request.ip == "1.2.3.4"
7
+ end
8
+ end
9
+
10
+ it "forbids request if blocklist condition is true" do
11
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
12
+
13
+ assert_equal 403, last_response.status
14
+ end
15
+
16
+ it "succeeds if blocklist condition is false" do
17
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
18
+
19
+ assert_equal 200, last_response.status
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
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,126 @@
1
+ require_relative "../spec_helper"
2
+ require "minitest/stub_const"
3
+
4
+ describe "Cache store config when using allow2ban" do
5
+ before do
6
+ Rack::Attack.blocklist("allow2ban pentesters") do |request|
7
+ Rack::Attack::Allow2Ban.filter(request.ip, maxretry: 2, findtime: 30, bantime: 60) do
8
+ request.path.include?("scarce-resource")
9
+ end
10
+ end
11
+ end
12
+
13
+ it "gives semantic error if no store was configured" do
14
+ assert_raises(Rack::Attack::MissingStoreError) do
15
+ get "/scarce-resource"
16
+ end
17
+ end
18
+
19
+ it "gives semantic error if store is missing #read method" do
20
+ raised_exception = nil
21
+
22
+ fake_store_class = Class.new do
23
+ def write(key, value)
24
+ end
25
+
26
+ def increment(key, count, options = {})
27
+ end
28
+ end
29
+
30
+ Object.stub_const(:FakeStore, fake_store_class) do
31
+ Rack::Attack.cache.store = FakeStore.new
32
+
33
+ raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
34
+ get "/scarce-resource"
35
+ end
36
+ end
37
+
38
+ assert_equal "Configured store FakeStore doesn't respond to #read method", raised_exception.message
39
+ end
40
+
41
+ it "gives semantic error if store is missing #write method" do
42
+ raised_exception = nil
43
+
44
+ fake_store_class = Class.new do
45
+ def read(key)
46
+ end
47
+
48
+ def increment(key, count, options = {})
49
+ end
50
+ end
51
+
52
+ Object.stub_const(:FakeStore, fake_store_class) do
53
+ Rack::Attack.cache.store = FakeStore.new
54
+
55
+ raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
56
+ get "/scarce-resource"
57
+ end
58
+ end
59
+
60
+ assert_equal "Configured store FakeStore doesn't respond to #write method", raised_exception.message
61
+ end
62
+
63
+ it "gives semantic error if store is missing #increment method" do
64
+ raised_exception = nil
65
+
66
+ fake_store_class = Class.new do
67
+ def read(key)
68
+ end
69
+
70
+ def write(key, value)
71
+ end
72
+ end
73
+
74
+ Object.stub_const(:FakeStore, fake_store_class) do
75
+ Rack::Attack.cache.store = FakeStore.new
76
+
77
+ raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
78
+ get "/scarce-resource"
79
+ end
80
+ end
81
+
82
+ assert_equal "Configured store FakeStore doesn't respond to #increment method", raised_exception.message
83
+ end
84
+
85
+ it "works with any object that responds to #read, #write and #increment" do
86
+ fake_store_class = Class.new do
87
+ attr_accessor :backend
88
+
89
+ def initialize
90
+ @backend = {}
91
+ end
92
+
93
+ def read(key)
94
+ @backend[key]
95
+ end
96
+
97
+ def write(key, value, _options = {})
98
+ @backend[key] = value
99
+ end
100
+
101
+ def increment(key, _count, _options = {})
102
+ @backend[key] ||= 0
103
+ @backend[key] += 1
104
+ end
105
+ end
106
+
107
+ Object.stub_const(:FakeStore, fake_store_class) do
108
+ Rack::Attack.cache.store = FakeStore.new
109
+
110
+ get "/"
111
+ assert_equal 200, last_response.status
112
+
113
+ get "/scarce-resource"
114
+ assert_equal 200, last_response.status
115
+
116
+ get "/scarce-resource"
117
+ assert_equal 200, last_response.status
118
+
119
+ get "/scarce-resource"
120
+ assert_equal 403, last_response.status
121
+
122
+ get "/"
123
+ assert_equal 403, last_response.status
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,121 @@
1
+ require_relative "../spec_helper"
2
+ require "minitest/stub_const"
3
+
4
+ describe "Cache store config when using fail2ban" do
5
+ before do
6
+ Rack::Attack.blocklist("fail2ban pentesters") do |request|
7
+ Rack::Attack::Fail2Ban.filter(request.ip, maxretry: 2, findtime: 30, bantime: 60) do
8
+ request.path.include?("private-place")
9
+ end
10
+ end
11
+ end
12
+
13
+ it "gives semantic error if no store was configured" do
14
+ assert_raises(Rack::Attack::MissingStoreError) do
15
+ get "/private-place"
16
+ end
17
+ end
18
+
19
+ it "gives semantic error if store is missing #read method" do
20
+ raised_exception = nil
21
+
22
+ fake_store_class = Class.new do
23
+ def write(key, value)
24
+ end
25
+
26
+ def increment(key, count, options = {})
27
+ end
28
+ end
29
+
30
+ Object.stub_const(:FakeStore, fake_store_class) do
31
+ Rack::Attack.cache.store = FakeStore.new
32
+
33
+ raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
34
+ get "/private-place"
35
+ end
36
+ end
37
+
38
+ assert_equal "Configured store FakeStore doesn't respond to #read method", raised_exception.message
39
+ end
40
+
41
+ it "gives semantic error if store is missing #write method" do
42
+ raised_exception = nil
43
+
44
+ fake_store_class = Class.new do
45
+ def read(key)
46
+ end
47
+
48
+ def increment(key, count, options = {})
49
+ end
50
+ end
51
+
52
+ Object.stub_const(:FakeStore, fake_store_class) do
53
+ Rack::Attack.cache.store = FakeStore.new
54
+
55
+ raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
56
+ get "/private-place"
57
+ end
58
+ end
59
+
60
+ assert_equal "Configured store FakeStore doesn't respond to #write method", raised_exception.message
61
+ end
62
+
63
+ it "gives semantic error if store is missing #increment method" do
64
+ raised_exception = nil
65
+
66
+ fake_store_class = Class.new do
67
+ def read(key)
68
+ end
69
+
70
+ def write(key, value)
71
+ end
72
+ end
73
+
74
+ Object.stub_const(:FakeStore, fake_store_class) do
75
+ Rack::Attack.cache.store = FakeStore.new
76
+
77
+ raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
78
+ get "/private-place"
79
+ end
80
+ end
81
+
82
+ assert_equal "Configured store FakeStore doesn't respond to #increment method", raised_exception.message
83
+ end
84
+
85
+ it "works with any object that responds to #read, #write and #increment" do
86
+ FakeStore = Class.new do
87
+ attr_accessor :backend
88
+
89
+ def initialize
90
+ @backend = {}
91
+ end
92
+
93
+ def read(key)
94
+ @backend[key]
95
+ end
96
+
97
+ def write(key, value, _options = {})
98
+ @backend[key] = value
99
+ end
100
+
101
+ def increment(key, _count, _options = {})
102
+ @backend[key] ||= 0
103
+ @backend[key] += 1
104
+ end
105
+ end
106
+
107
+ Rack::Attack.cache.store = FakeStore.new
108
+
109
+ get "/"
110
+ assert_equal 200, last_response.status
111
+
112
+ get "/private-place"
113
+ assert_equal 403, last_response.status
114
+
115
+ get "/private-place"
116
+ assert_equal 403, last_response.status
117
+
118
+ get "/"
119
+ assert_equal 403, last_response.status
120
+ end
121
+ end
@@ -0,0 +1,48 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "Cache store config when throttling without Rails" do
4
+ before do
5
+ Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
6
+ request.ip
7
+ end
8
+ end
9
+
10
+ it "gives semantic error if no store was configured" do
11
+ assert_raises(Rack::Attack::MissingStoreError) do
12
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
13
+ end
14
+ end
15
+
16
+ it "gives semantic error if incompatible store was configured" do
17
+ Rack::Attack.cache.store = Object.new
18
+
19
+ assert_raises(Rack::Attack::MisconfiguredStoreError) do
20
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
21
+ end
22
+ end
23
+
24
+ it "works with any object that responds to #increment" do
25
+ basic_store_class = Class.new do
26
+ attr_accessor :counts
27
+
28
+ def initialize
29
+ @counts = {}
30
+ end
31
+
32
+ def increment(key, _count, _options)
33
+ @counts[key] ||= 0
34
+ @counts[key] += 1
35
+ end
36
+ end
37
+
38
+ Rack::Attack.cache.store = basic_store_class.new
39
+
40
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
41
+
42
+ assert_equal 200, last_response.status
43
+
44
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
45
+
46
+ assert_equal 429, last_response.status
47
+ end
48
+ end
@@ -0,0 +1,31 @@
1
+ require_relative "../spec_helper"
2
+ require "minitest/stub_const"
3
+ require "ostruct"
4
+
5
+ describe "Cache store config with Rails" do
6
+ before do
7
+ Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
8
+ request.ip
9
+ end
10
+ end
11
+
12
+ it "fails when Rails.cache is not set" do
13
+ Object.stub_const(:Rails, OpenStruct.new(cache: nil)) do
14
+ assert_raises(Rack::Attack::MissingStoreError) do
15
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
16
+ end
17
+ end
18
+ end
19
+
20
+ it "works when Rails.cache is set" do
21
+ Object.stub_const(:Rails, OpenStruct.new(cache: ActiveSupport::Cache::MemoryStore.new)) do
22
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
23
+
24
+ assert_equal 200, last_response.status
25
+
26
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
27
+
28
+ assert_equal 429, last_response.status
29
+ end
30
+ end
31
+ end