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,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
@@ -0,0 +1,34 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "Extending the request object" do
4
+ before do
5
+ class Rack::Attack::Request
6
+ def authorized?
7
+ env["APIKey"] == "private-secret"
8
+ end
9
+ end
10
+
11
+ Rack::Attack.blocklist("unauthorized requests") do |request|
12
+ !request.authorized?
13
+ end
14
+ end
15
+
16
+ # We don't want the extension to leak to other test cases
17
+ after do
18
+ class Rack::Attack::Request
19
+ remove_method :authorized?
20
+ end
21
+ end
22
+
23
+ it "forbids request if blocklist condition is true" do
24
+ get "/"
25
+
26
+ assert_equal 403, last_response.status
27
+ end
28
+
29
+ it "succeeds if blocklist condition is false" do
30
+ get "/", {}, "APIKey" => "private-secret"
31
+
32
+ assert_equal 200, last_response.status
33
+ end
34
+ end
@@ -0,0 +1,76 @@
1
+ require_relative "../spec_helper"
2
+ require "timecop"
3
+
4
+ describe "fail2ban" do
5
+ before do
6
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
7
+
8
+ Rack::Attack.blocklist("fail2ban pentesters") do |request|
9
+ Rack::Attack::Fail2Ban.filter(request.ip, maxretry: 2, findtime: 30, bantime: 60) do
10
+ request.path.include?("private-place")
11
+ end
12
+ end
13
+ end
14
+
15
+ it "returns OK for many requests to non filtered path" 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 "forbids access to private path" do
24
+ get "/private-place"
25
+ assert_equal 403, last_response.status
26
+ end
27
+
28
+ it "returns OK for non filtered path if yet not reached maxretry limit" do
29
+ get "/private-place"
30
+ assert_equal 403, last_response.status
31
+
32
+ get "/"
33
+ assert_equal 200, last_response.status
34
+ end
35
+
36
+ it "forbids all access after reaching maxretry limit" do
37
+ get "/private-place"
38
+ assert_equal 403, last_response.status
39
+
40
+ get "/private-place"
41
+ assert_equal 403, last_response.status
42
+
43
+ get "/"
44
+ assert_equal 403, last_response.status
45
+ end
46
+
47
+ it "restores access after bantime elapsed" do
48
+ get "/private-place"
49
+ assert_equal 403, last_response.status
50
+
51
+ get "/private-place"
52
+ assert_equal 403, last_response.status
53
+
54
+ get "/"
55
+ assert_equal 403, last_response.status
56
+
57
+ Timecop.travel(60) do
58
+ get "/"
59
+
60
+ assert_equal 200, last_response.status
61
+ end
62
+ end
63
+
64
+ it "does not forbid all access if maxrety condition is met but not within the findtime timespan" do
65
+ get "/private-place"
66
+ assert_equal 403, last_response.status
67
+
68
+ Timecop.travel(31) do
69
+ get "/private-place"
70
+ assert_equal 403, last_response.status
71
+
72
+ get "/"
73
+ assert_equal 200, last_response.status
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,48 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "Safelist an IP" do
4
+ before do
5
+ Rack::Attack.blocklist("admin") do |request|
6
+ request.path == "/admin"
7
+ end
8
+
9
+ Rack::Attack.safelist_ip("5.6.7.8")
10
+ end
11
+
12
+ it "forbids request if blocklist condition is true and safelist is false" do
13
+ get "/admin", {}, "REMOTE_ADDR" => "1.2.3.4"
14
+
15
+ assert_equal 403, last_response.status
16
+ end
17
+
18
+ it "succeeds if blocklist condition is false and safelist is false" do
19
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
20
+
21
+ assert_equal 200, last_response.status
22
+ end
23
+
24
+ it "succeeds request if blocklist condition is false and safelist is true" do
25
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
26
+
27
+ assert_equal 200, last_response.status
28
+ end
29
+
30
+ it "succeeds request if both blocklist and safelist conditions are true" do
31
+ get "/admin", {}, "REMOTE_ADDR" => "5.6.7.8"
32
+
33
+ assert_equal 200, last_response.status
34
+ end
35
+
36
+ it "notifies when the request is safe" do
37
+ notification_type = nil
38
+
39
+ ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
40
+ notification_type = request.env["rack.attack.match_type"]
41
+ end
42
+
43
+ get "/admin", {}, "REMOTE_ADDR" => "5.6.7.8"
44
+
45
+ assert_equal 200, last_response.status
46
+ assert_equal :safelist, notification_type
47
+ end
48
+ end
@@ -0,0 +1,53 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "#safelist" 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
+
9
+ Rack::Attack.safelist("safe path") do |request|
10
+ request.path == "/safe_space"
11
+ end
12
+ end
13
+
14
+ it "forbids request if blocklist condition is true and safelist is false" do
15
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
16
+
17
+ assert_equal 403, last_response.status
18
+ end
19
+
20
+ it "succeeds if blocklist condition is false and safelist is false" do
21
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
22
+
23
+ assert_equal 200, last_response.status
24
+ end
25
+
26
+ it "succeeds request if blocklist condition is false and safelist is true" do
27
+ get "/safe_space", {}, "REMOTE_ADDR" => "5.6.7.8"
28
+
29
+ assert_equal 200, last_response.status
30
+ end
31
+
32
+ it "succeeds request if both blocklist and safelist conditions are true" do
33
+ get "/safe_space", {}, "REMOTE_ADDR" => "1.2.3.4"
34
+
35
+ assert_equal 200, last_response.status
36
+ end
37
+
38
+ it "notifies when the request is safe" do
39
+ notification_matched = nil
40
+ notification_type = nil
41
+
42
+ ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
43
+ notification_matched = request.env["rack.attack.matched"]
44
+ notification_type = request.env["rack.attack.match_type"]
45
+ end
46
+
47
+ get "/safe_space", {}, "REMOTE_ADDR" => "1.2.3.4"
48
+
49
+ assert_equal 200, last_response.status
50
+ assert_equal "safe path", notification_matched
51
+ assert_equal :safelist, notification_type
52
+ end
53
+ end
@@ -0,0 +1,48 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "Safelisting an IP subnet" do
4
+ before do
5
+ Rack::Attack.blocklist("admin") do |request|
6
+ request.path == "/admin"
7
+ end
8
+
9
+ Rack::Attack.safelist_ip("5.6.0.0/16")
10
+ end
11
+
12
+ it "forbids request if blocklist condition is true and safelist is false" do
13
+ get "/admin", {}, "REMOTE_ADDR" => "5.7.0.0"
14
+
15
+ assert_equal 403, last_response.status
16
+ end
17
+
18
+ it "succeeds if blocklist condition is false and safelist is false" do
19
+ get "/", {}, "REMOTE_ADDR" => "5.7.0.0"
20
+
21
+ assert_equal 200, last_response.status
22
+ end
23
+
24
+ it "succeeds request if blocklist condition is false and safelist is true" do
25
+ get "/", {}, "REMOTE_ADDR" => "5.6.0.0"
26
+
27
+ assert_equal 200, last_response.status
28
+ end
29
+
30
+ it "succeeds request if both blocklist and safelist conditions are true" do
31
+ get "/admin", {}, "REMOTE_ADDR" => "5.6.255.255"
32
+
33
+ assert_equal 200, last_response.status
34
+ end
35
+
36
+ it "notifies when the request is safe" do
37
+ notification_type = nil
38
+
39
+ ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
40
+ notification_type = request.env["rack.attack.match_type"]
41
+ end
42
+
43
+ get "/admin", {}, "REMOTE_ADDR" => "5.6.0.0"
44
+
45
+ assert_equal 200, last_response.status
46
+ assert_equal :safelist, notification_type
47
+ end
48
+ end
@@ -0,0 +1,19 @@
1
+ require_relative "../../spec_helper"
2
+
3
+ if defined?(::Dalli)
4
+ require_relative "../../support/cache_store_helper"
5
+ require "active_support/cache/dalli_store"
6
+ require "timecop"
7
+
8
+ describe "ActiveSupport::Cache::DalliStore as a cache backend" do
9
+ before do
10
+ Rack::Attack.cache.store = ActiveSupport::Cache::DalliStore.new
11
+ end
12
+
13
+ after do
14
+ Rack::Attack.cache.store.clear
15
+ end
16
+
17
+ it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.fetch(key) })
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../spec_helper"
4
+
5
+ if defined?(::ConnectionPool) && defined?(::Dalli)
6
+ require_relative "../../support/cache_store_helper"
7
+ require "timecop"
8
+
9
+ describe "ActiveSupport::Cache::MemCacheStore (pooled) as a cache backend" do
10
+ before do
11
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemCacheStore.new(pool_size: 2)
12
+ end
13
+
14
+ after do
15
+ Rack::Attack.cache.store.clear
16
+ end
17
+
18
+ it_works_for_cache_backed_features(fetch_from_store: ->(key) {
19
+ Rack::Attack.cache.store.read(key)
20
+ })
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ require_relative "../../spec_helper"
2
+
3
+ if defined?(::Dalli)
4
+ require_relative "../../support/cache_store_helper"
5
+ require "timecop"
6
+
7
+ describe "ActiveSupport::Cache::MemCacheStore as a cache backend" do
8
+ before do
9
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemCacheStore.new
10
+ end
11
+
12
+ after do
13
+ Rack::Attack.cache.store.clear
14
+ end
15
+
16
+ it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.read(key) })
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ require_relative "../../spec_helper"
2
+ require_relative "../../support/cache_store_helper"
3
+
4
+ require "timecop"
5
+
6
+ describe "ActiveSupport::Cache::MemoryStore as a cache backend" do
7
+ before do
8
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
9
+ end
10
+
11
+ after do
12
+ Rack::Attack.cache.store.clear
13
+ end
14
+
15
+ it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.fetch(key) })
16
+ end