rack-attack 5.4.0 → 6.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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +78 -27
  3. data/Rakefile +3 -1
  4. data/bin/setup +8 -0
  5. data/lib/rack/attack.rb +137 -148
  6. data/lib/rack/attack/allow2ban.rb +2 -0
  7. data/lib/rack/attack/blocklist.rb +3 -1
  8. data/lib/rack/attack/cache.rb +9 -4
  9. data/lib/rack/attack/check.rb +5 -2
  10. data/lib/rack/attack/fail2ban.rb +2 -0
  11. data/lib/rack/attack/path_normalizer.rb +22 -18
  12. data/lib/rack/attack/railtie.rb +21 -0
  13. data/lib/rack/attack/request.rb +2 -0
  14. data/lib/rack/attack/safelist.rb +3 -1
  15. data/lib/rack/attack/store_proxy.rb +12 -24
  16. data/lib/rack/attack/store_proxy/active_support_redis_store_proxy.rb +39 -0
  17. data/lib/rack/attack/store_proxy/dalli_proxy.rb +27 -13
  18. data/lib/rack/attack/store_proxy/mem_cache_store_proxy.rb +21 -0
  19. data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +23 -9
  20. data/lib/rack/attack/store_proxy/redis_proxy.rb +16 -10
  21. data/lib/rack/attack/store_proxy/redis_store_proxy.rb +5 -5
  22. data/lib/rack/attack/throttle.rb +12 -8
  23. data/lib/rack/attack/track.rb +9 -6
  24. data/lib/rack/attack/version.rb +3 -1
  25. data/spec/acceptance/allow2ban_spec.rb +2 -0
  26. data/spec/acceptance/blocking_ip_spec.rb +4 -2
  27. data/spec/acceptance/blocking_spec.rb +45 -3
  28. data/spec/acceptance/blocking_subnet_spec.rb +4 -2
  29. data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +50 -39
  30. data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +38 -29
  31. data/spec/acceptance/cache_store_config_for_throttle_spec.rb +2 -0
  32. data/spec/acceptance/cache_store_config_with_rails_spec.rb +2 -0
  33. data/spec/acceptance/customizing_blocked_response_spec.rb +2 -0
  34. data/spec/acceptance/customizing_throttled_response_spec.rb +2 -0
  35. data/spec/acceptance/extending_request_object_spec.rb +2 -0
  36. data/spec/acceptance/fail2ban_spec.rb +2 -0
  37. data/spec/acceptance/rails_middleware_spec.rb +41 -0
  38. data/spec/acceptance/safelisting_ip_spec.rb +4 -2
  39. data/spec/acceptance/safelisting_spec.rb +57 -3
  40. data/spec/acceptance/safelisting_subnet_spec.rb +4 -2
  41. data/spec/acceptance/stores/active_support_dalli_store_spec.rb +3 -23
  42. data/spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb +20 -0
  43. data/spec/acceptance/stores/active_support_mem_cache_store_spec.rb +4 -24
  44. data/spec/acceptance/stores/active_support_memory_store_spec.rb +3 -23
  45. data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +10 -24
  46. data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +9 -25
  47. data/spec/acceptance/stores/active_support_redis_store_spec.rb +4 -24
  48. data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +5 -23
  49. data/spec/acceptance/stores/dalli_client_spec.rb +3 -23
  50. data/spec/acceptance/stores/redis_spec.rb +1 -23
  51. data/spec/acceptance/stores/redis_store_spec.rb +3 -23
  52. data/spec/acceptance/throttling_spec.rb +7 -5
  53. data/spec/acceptance/track_spec.rb +5 -3
  54. data/spec/acceptance/track_throttle_spec.rb +5 -3
  55. data/spec/allow2ban_spec.rb +20 -15
  56. data/spec/fail2ban_spec.rb +20 -17
  57. data/spec/integration/offline_spec.rb +3 -1
  58. data/spec/rack_attack_dalli_proxy_spec.rb +2 -0
  59. data/spec/rack_attack_instrumentation_spec.rb +42 -0
  60. data/spec/rack_attack_path_normalizer_spec.rb +4 -2
  61. data/spec/rack_attack_request_spec.rb +2 -0
  62. data/spec/rack_attack_spec.rb +38 -34
  63. data/spec/rack_attack_throttle_spec.rb +50 -19
  64. data/spec/rack_attack_track_spec.rb +12 -7
  65. data/spec/spec_helper.rb +10 -8
  66. data/spec/support/cache_store_helper.rb +27 -1
  67. metadata +48 -28
  68. data/lib/rack/attack/store_proxy/mem_cache_proxy.rb +0 -50
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
 
3
5
  describe "Cache store config when throttling without Rails" do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
  require "minitest/stub_const"
3
5
  require "ostruct"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
 
3
5
  describe "Customizing block responses" do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
 
3
5
  describe "Customizing throttled response" do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
 
3
5
  describe "Extending the request object" do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
  require "timecop"
3
5
 
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../spec_helper"
4
+
5
+ if defined?(Rails)
6
+ describe "Middleware for Rails" do
7
+ before do
8
+ @app = Class.new(Rails::Application) do
9
+ config.eager_load = false
10
+ config.logger = Logger.new(nil) # avoid creating the log/ directory automatically
11
+ config.cache_store = :null_store # avoid creating tmp/ directory for cache
12
+ end
13
+ end
14
+
15
+ if Gem::Version.new(Rails::VERSION::STRING) >= Gem::Version.new("5.1")
16
+ it "is used by default" do
17
+ @app.initialize!
18
+ assert_equal 1, @app.middleware.count(Rack::Attack)
19
+ end
20
+
21
+ it "is not added when it was added explicitly" do
22
+ @app.config.middleware.use(Rack::Attack)
23
+ @app.initialize!
24
+ assert_equal 1, @app.middleware.count(Rack::Attack)
25
+ end
26
+
27
+ it "is not added when it was explicitly deleted" do
28
+ @app.config.middleware.delete(Rack::Attack)
29
+ @app.initialize!
30
+ refute @app.middleware.include?(Rack::Attack)
31
+ end
32
+ end
33
+
34
+ if Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new("5.1")
35
+ it "is not used by default" do
36
+ @app.initialize!
37
+ assert_equal 0, @app.middleware.count(Rack::Attack)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
 
3
5
  describe "Safelist an IP" do
@@ -36,8 +38,8 @@ describe "Safelist an IP" do
36
38
  it "notifies when the request is safe" do
37
39
  notification_type = nil
38
40
 
39
- ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
40
- notification_type = request.env["rack.attack.match_type"]
41
+ ActiveSupport::Notifications.subscribe("safelist.rack_attack") do |_name, _start, _finish, _id, payload|
42
+ notification_type = payload[:request].env["rack.attack.match_type"]
41
43
  end
42
44
 
43
45
  get "/admin", {}, "REMOTE_ADDR" => "5.6.7.8"
@@ -1,6 +1,60 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
 
3
5
  describe "#safelist" do
6
+ before do
7
+ Rack::Attack.blocklist do |request|
8
+ request.ip == "1.2.3.4"
9
+ end
10
+
11
+ Rack::Attack.safelist do |request|
12
+ request.path == "/safe_space"
13
+ end
14
+ end
15
+
16
+ it "forbids request if blocklist condition is true and safelist is false" do
17
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
18
+
19
+ assert_equal 403, last_response.status
20
+ end
21
+
22
+ it "succeeds if blocklist condition is false and safelist is false" do
23
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
24
+
25
+ assert_equal 200, last_response.status
26
+ end
27
+
28
+ it "succeeds request if blocklist condition is false and safelist is true" do
29
+ get "/safe_space", {}, "REMOTE_ADDR" => "5.6.7.8"
30
+
31
+ assert_equal 200, last_response.status
32
+ end
33
+
34
+ it "succeeds request if both blocklist and safelist conditions are true" do
35
+ get "/safe_space", {}, "REMOTE_ADDR" => "1.2.3.4"
36
+
37
+ assert_equal 200, last_response.status
38
+ end
39
+
40
+ it "notifies when the request is safe" do
41
+ notification_matched = nil
42
+ notification_type = nil
43
+
44
+ ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, payload|
45
+ notification_matched = payload[:request].env["rack.attack.matched"]
46
+ notification_type = payload[:request].env["rack.attack.match_type"]
47
+ end
48
+
49
+ get "/safe_space", {}, "REMOTE_ADDR" => "1.2.3.4"
50
+
51
+ assert_equal 200, last_response.status
52
+ assert_nil notification_matched
53
+ assert_equal :safelist, notification_type
54
+ end
55
+ end
56
+
57
+ describe "#safelist with name" do
4
58
  before do
5
59
  Rack::Attack.blocklist("block 1.2.3.4") do |request|
6
60
  request.ip == "1.2.3.4"
@@ -39,9 +93,9 @@ describe "#safelist" do
39
93
  notification_matched = nil
40
94
  notification_type = nil
41
95
 
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"]
96
+ ActiveSupport::Notifications.subscribe("safelist.rack_attack") do |_name, _start, _finish, _id, payload|
97
+ notification_matched = payload[:request].env["rack.attack.matched"]
98
+ notification_type = payload[:request].env["rack.attack.match_type"]
45
99
  end
46
100
 
47
101
  get "/safe_space", {}, "REMOTE_ADDR" => "1.2.3.4"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
 
3
5
  describe "Safelisting an IP subnet" do
@@ -36,8 +38,8 @@ describe "Safelisting an IP subnet" do
36
38
  it "notifies when the request is safe" do
37
39
  notification_type = nil
38
40
 
39
- ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
40
- notification_type = request.env["rack.attack.match_type"]
41
+ ActiveSupport::Notifications.subscribe("safelist.rack_attack") do |_name, _start, _finish, _id, payload|
42
+ notification_type = payload[:request].env["rack.attack.match_type"]
41
43
  end
42
44
 
43
45
  get "/admin", {}, "REMOTE_ADDR" => "5.6.0.0"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../../spec_helper"
2
4
 
3
5
  if defined?(::Dalli)
@@ -14,28 +16,6 @@ if defined?(::Dalli)
14
16
  Rack::Attack.cache.store.clear
15
17
  end
16
18
 
17
- it_works_for_cache_backed_features
18
-
19
- it "doesn't leak keys" do
20
- Rack::Attack.throttle("by ip", limit: 1, period: 1) do |request|
21
- request.ip
22
- end
23
-
24
- key = nil
25
-
26
- # Freeze time during these statement to be sure that the key used by rack attack is the same
27
- # we pre-calculate in local variable `key`
28
- Timecop.freeze do
29
- key = "rack::attack:#{Time.now.to_i}:by ip:1.2.3.4"
30
-
31
- get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
32
- end
33
-
34
- assert Rack::Attack.cache.store.fetch(key)
35
-
36
- sleep 2.1
37
-
38
- assert_nil Rack::Attack.cache.store.fetch(key)
39
- end
19
+ it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.fetch(key) })
40
20
  end
41
21
  end
@@ -0,0 +1,20 @@
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) { Rack::Attack.cache.store.read(key) })
19
+ end
20
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../../spec_helper"
2
4
 
3
5
  if defined?(::Dalli)
@@ -10,31 +12,9 @@ if defined?(::Dalli)
10
12
  end
11
13
 
12
14
  after do
13
- Rack::Attack.cache.store.flush_all
15
+ Rack::Attack.cache.store.clear
14
16
  end
15
17
 
16
- it_works_for_cache_backed_features
17
-
18
- it "doesn't leak keys" do
19
- Rack::Attack.throttle("by ip", limit: 1, period: 1) do |request|
20
- request.ip
21
- end
22
-
23
- key = nil
24
-
25
- # Freeze time during these statement to be sure that the key used by rack attack is the same
26
- # we pre-calculate in local variable `key`
27
- Timecop.freeze do
28
- key = "rack::attack:#{Time.now.to_i}:by ip:1.2.3.4"
29
-
30
- get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
31
- end
32
-
33
- assert Rack::Attack.cache.store.get(key)
34
-
35
- sleep 2.1
36
-
37
- assert_nil Rack::Attack.cache.store.get(key)
38
- end
18
+ it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.read(key) })
39
19
  end
40
20
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../../spec_helper"
2
4
  require_relative "../../support/cache_store_helper"
3
5
 
@@ -12,27 +14,5 @@ describe "ActiveSupport::Cache::MemoryStore as a cache backend" do
12
14
  Rack::Attack.cache.store.clear
13
15
  end
14
16
 
15
- it_works_for_cache_backed_features
16
-
17
- it "doesn't leak keys" do
18
- Rack::Attack.throttle("by ip", limit: 1, period: 1) do |request|
19
- request.ip
20
- end
21
-
22
- key = nil
23
-
24
- # Freeze time during these statement to be sure that the key used by rack attack is the same
25
- # we pre-calculate in local variable `key`
26
- Timecop.freeze do
27
- key = "rack::attack:#{Time.now.to_i}:by ip:1.2.3.4"
28
-
29
- get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
30
- end
31
-
32
- assert Rack::Attack.cache.store.fetch(key)
33
-
34
- sleep 2.1
35
-
36
- assert_nil Rack::Attack.cache.store.fetch(key)
37
- end
17
+ it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.fetch(key) })
38
18
  end
@@ -1,6 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../../spec_helper"
2
4
 
3
- if defined?(::ConnectionPool) && defined?(::Redis) && defined?(::ActiveSupport::Cache::RedisCacheStore)
5
+ should_run =
6
+ defined?(::ConnectionPool) &&
7
+ defined?(::Redis) &&
8
+ Gem::Version.new(::Redis::VERSION) >= Gem::Version.new("4") &&
9
+ defined?(::ActiveSupport::Cache::RedisCacheStore)
10
+
11
+ if should_run
4
12
  require_relative "../../support/cache_store_helper"
5
13
  require "timecop"
6
14
 
@@ -13,28 +21,6 @@ if defined?(::ConnectionPool) && defined?(::Redis) && defined?(::ActiveSupport::
13
21
  Rack::Attack.cache.store.clear
14
22
  end
15
23
 
16
- it_works_for_cache_backed_features
17
-
18
- it "doesn't leak keys" do
19
- Rack::Attack.throttle("by ip", limit: 1, period: 1) do |request|
20
- request.ip
21
- end
22
-
23
- key = nil
24
-
25
- # Freeze time during these statement to be sure that the key used by rack attack is the same
26
- # we pre-calculate in local variable `key`
27
- Timecop.freeze do
28
- key = "rack::attack:#{Time.now.to_i}:by ip:1.2.3.4"
29
-
30
- get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
31
- end
32
-
33
- assert Rack::Attack.cache.store.fetch(key)
34
-
35
- sleep 2.1
36
-
37
- assert_nil Rack::Attack.cache.store.fetch(key)
38
- end
24
+ it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.fetch(key) })
39
25
  end
40
26
  end
@@ -1,6 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../../spec_helper"
2
4
 
3
- if defined?(::Redis) && defined?(::ActiveSupport::Cache::RedisCacheStore)
5
+ should_run =
6
+ defined?(::Redis) &&
7
+ Gem::Version.new(::Redis::VERSION) >= Gem::Version.new("4") &&
8
+ defined?(::ActiveSupport::Cache::RedisCacheStore)
9
+
10
+ if should_run
4
11
  require_relative "../../support/cache_store_helper"
5
12
  require "timecop"
6
13
 
@@ -13,29 +20,6 @@ if defined?(::Redis) && defined?(::ActiveSupport::Cache::RedisCacheStore)
13
20
  Rack::Attack.cache.store.clear
14
21
  end
15
22
 
16
- it_works_for_cache_backed_features
17
-
18
- it "doesn't leak keys" do
19
- Rack::Attack.throttle("by ip", limit: 1, period: 1) do |request|
20
- request.ip
21
- end
22
-
23
- key = nil
24
-
25
- # Freeze time during these statement to be sure that the key used by rack attack is the same
26
- # we pre-calculate in local variable `key`
27
- Timecop.freeze do
28
- key = "rack::attack:#{Time.now.to_i}:by ip:1.2.3.4"
29
-
30
- # puts key
31
- get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
32
- end
33
-
34
- assert Rack::Attack.cache.store.fetch(key)
35
-
36
- sleep 2.1
37
-
38
- assert_nil Rack::Attack.cache.store.fetch(key)
39
- end
23
+ it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.fetch(key) })
40
24
  end
41
25
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../../spec_helper"
2
4
 
3
5
  if defined?(::ActiveSupport::Cache::RedisStore)
@@ -10,31 +12,9 @@ if defined?(::ActiveSupport::Cache::RedisStore)
10
12
  end
11
13
 
12
14
  after do
13
- Rack::Attack.cache.store.flushdb
15
+ Rack::Attack.cache.store.clear
14
16
  end
15
17
 
16
- it_works_for_cache_backed_features
17
-
18
- it "doesn't leak keys" do
19
- Rack::Attack.throttle("by ip", limit: 1, period: 1) do |request|
20
- request.ip
21
- end
22
-
23
- key = nil
24
-
25
- # Freeze time during these statement to be sure that the key used by rack attack is the same
26
- # we pre-calculate in local variable `key`
27
- Timecop.freeze do
28
- key = "rack::attack:#{Time.now.to_i}:by ip:1.2.3.4"
29
-
30
- get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
31
- end
32
-
33
- assert Rack::Attack.cache.store.read(key)
34
-
35
- sleep 2.1
36
-
37
- assert_nil Rack::Attack.cache.store.read(key)
38
- end
18
+ it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.read(key) })
39
19
  end
40
20
  end