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
@@ -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
@@ -0,0 +1,41 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "Customizing block responses" 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 "can be customized" do
11
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
12
+
13
+ assert_equal 403, last_response.status
14
+
15
+ Rack::Attack.blocklisted_response = lambda do |_env|
16
+ [503, {}, ["Blocked"]]
17
+ end
18
+
19
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
20
+
21
+ assert_equal 503, last_response.status
22
+ assert_equal "Blocked", last_response.body
23
+ end
24
+
25
+ it "exposes match data" do
26
+ matched = nil
27
+ match_type = nil
28
+
29
+ Rack::Attack.blocklisted_response = lambda do |env|
30
+ matched = env['rack.attack.matched']
31
+ match_type = env['rack.attack.match_type']
32
+
33
+ [503, {}, ["Blocked"]]
34
+ end
35
+
36
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
37
+
38
+ assert_equal "block 1.2.3.4", matched
39
+ assert_equal :blocklist, match_type
40
+ end
41
+ end
@@ -0,0 +1,59 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "Customizing throttled response" do
4
+ before do
5
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
6
+
7
+ Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
8
+ request.ip
9
+ end
10
+ end
11
+
12
+ it "can be customized" do
13
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
14
+
15
+ assert_equal 200, last_response.status
16
+
17
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
18
+
19
+ assert_equal 429, last_response.status
20
+
21
+ Rack::Attack.throttled_response = lambda do |_env|
22
+ [503, {}, ["Throttled"]]
23
+ end
24
+
25
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
26
+
27
+ assert_equal 503, last_response.status
28
+ assert_equal "Throttled", last_response.body
29
+ end
30
+
31
+ it "exposes match data" do
32
+ matched = nil
33
+ match_type = nil
34
+ match_data = nil
35
+ match_discriminator = nil
36
+
37
+ Rack::Attack.throttled_response = lambda do |env|
38
+ matched = env['rack.attack.matched']
39
+ match_type = env['rack.attack.match_type']
40
+ match_data = env['rack.attack.match_data']
41
+ match_discriminator = env['rack.attack.match_discriminator']
42
+
43
+ [429, {}, ["Throttled"]]
44
+ end
45
+
46
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
47
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
48
+
49
+ assert_equal "by ip", matched
50
+ assert_equal :throttle, match_type
51
+ assert_equal 60, match_data[:period]
52
+ assert_equal 1, match_data[:limit]
53
+ assert_equal 2, match_data[:count]
54
+ assert_equal "1.2.3.4", match_discriminator
55
+
56
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
57
+ assert_equal 3, match_data[:count]
58
+ end
59
+ end