rack-attack 5.4.1 → 6.2.1

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +78 -27
  3. data/Rakefile +3 -1
  4. data/lib/rack/attack.rb +138 -149
  5. data/lib/rack/attack/allow2ban.rb +2 -0
  6. data/lib/rack/attack/blocklist.rb +3 -1
  7. data/lib/rack/attack/cache.rb +9 -4
  8. data/lib/rack/attack/check.rb +5 -2
  9. data/lib/rack/attack/fail2ban.rb +2 -0
  10. data/lib/rack/attack/path_normalizer.rb +22 -18
  11. data/lib/rack/attack/railtie.rb +13 -0
  12. data/lib/rack/attack/request.rb +2 -0
  13. data/lib/rack/attack/safelist.rb +3 -1
  14. data/lib/rack/attack/store_proxy.rb +12 -14
  15. data/lib/rack/attack/store_proxy/active_support_redis_store_proxy.rb +39 -0
  16. data/lib/rack/attack/store_proxy/dalli_proxy.rb +27 -13
  17. data/lib/rack/attack/store_proxy/mem_cache_store_proxy.rb +3 -1
  18. data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +23 -9
  19. data/lib/rack/attack/store_proxy/redis_proxy.rb +16 -10
  20. data/lib/rack/attack/store_proxy/redis_store_proxy.rb +5 -5
  21. data/lib/rack/attack/throttle.rb +12 -8
  22. data/lib/rack/attack/track.rb +9 -6
  23. data/lib/rack/attack/version.rb +3 -1
  24. data/spec/acceptance/allow2ban_spec.rb +2 -0
  25. data/spec/acceptance/blocking_ip_spec.rb +4 -2
  26. data/spec/acceptance/blocking_spec.rb +45 -3
  27. data/spec/acceptance/blocking_subnet_spec.rb +4 -2
  28. data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +50 -39
  29. data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +38 -29
  30. data/spec/acceptance/cache_store_config_for_throttle_spec.rb +2 -0
  31. data/spec/acceptance/cache_store_config_with_rails_spec.rb +2 -0
  32. data/spec/acceptance/customizing_blocked_response_spec.rb +2 -0
  33. data/spec/acceptance/customizing_throttled_response_spec.rb +2 -0
  34. data/spec/acceptance/extending_request_object_spec.rb +2 -0
  35. data/spec/acceptance/fail2ban_spec.rb +2 -0
  36. data/spec/acceptance/rails_middleware_spec.rb +35 -0
  37. data/spec/acceptance/safelisting_ip_spec.rb +4 -2
  38. data/spec/acceptance/safelisting_spec.rb +57 -3
  39. data/spec/acceptance/safelisting_subnet_spec.rb +4 -2
  40. data/spec/acceptance/stores/active_support_dalli_store_spec.rb +2 -0
  41. data/spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb +1 -3
  42. data/spec/acceptance/stores/active_support_mem_cache_store_spec.rb +2 -0
  43. data/spec/acceptance/stores/active_support_memory_store_spec.rb +2 -0
  44. data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +9 -1
  45. data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +8 -1
  46. data/spec/acceptance/stores/active_support_redis_store_spec.rb +3 -1
  47. data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +5 -3
  48. data/spec/acceptance/stores/dalli_client_spec.rb +2 -0
  49. data/spec/acceptance/stores/redis_store_spec.rb +2 -0
  50. data/spec/acceptance/throttling_spec.rb +7 -5
  51. data/spec/acceptance/track_spec.rb +5 -3
  52. data/spec/acceptance/track_throttle_spec.rb +5 -3
  53. data/spec/allow2ban_spec.rb +20 -15
  54. data/spec/fail2ban_spec.rb +20 -17
  55. data/spec/integration/offline_spec.rb +3 -1
  56. data/spec/rack_attack_dalli_proxy_spec.rb +2 -0
  57. data/spec/rack_attack_instrumentation_spec.rb +42 -0
  58. data/spec/rack_attack_path_normalizer_spec.rb +4 -2
  59. data/spec/rack_attack_request_spec.rb +2 -0
  60. data/spec/rack_attack_spec.rb +38 -34
  61. data/spec/rack_attack_throttle_spec.rb +50 -19
  62. data/spec/rack_attack_track_spec.rb +12 -7
  63. data/spec/spec_helper.rb +12 -8
  64. data/spec/support/cache_store_helper.rb +2 -0
  65. metadata +44 -28
  66. 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 "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,35 @@
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 explicitly deleted" do
22
+ @app.config.middleware.delete(Rack::Attack)
23
+ @app.initialize!
24
+ refute @app.middleware.include?(Rack::Attack)
25
+ end
26
+ end
27
+
28
+ if Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new("5.1")
29
+ it "is not used by default" do
30
+ @app.initialize!
31
+ assert_equal 0, @app.middleware.count(Rack::Attack)
32
+ end
33
+ end
34
+ end
35
+ 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)
@@ -15,8 +15,6 @@ if defined?(::ConnectionPool) && defined?(::Dalli)
15
15
  Rack::Attack.cache.store.clear
16
16
  end
17
17
 
18
- it_works_for_cache_backed_features(fetch_from_store: ->(key) {
19
- Rack::Attack.cache.store.read(key)
20
- })
18
+ it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.read(key) })
21
19
  end
22
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)
@@ -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
 
@@ -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
 
@@ -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
 
@@ -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,7 +12,7 @@ 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
18
  it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.read(key) })
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../../spec_helper"
2
4
 
3
5
  if defined?(::Dalli) && defined?(::ConnectionPool)
@@ -15,8 +17,8 @@ if defined?(::Dalli) && defined?(::ConnectionPool)
15
17
  Rack::Attack.cache.store.with { |client| client.flush_all }
16
18
  end
17
19
 
18
- it_works_for_cache_backed_features(fetch_from_store: ->(key) {
19
- Rack::Attack.cache.store.with { |client| client.fetch(key) }
20
- })
20
+ it_works_for_cache_backed_features(
21
+ fetch_from_store: ->(key) { Rack::Attack.cache.store.with { |client| client.fetch(key) } }
22
+ )
21
23
  end
22
24
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../../spec_helper"
2
4
 
3
5
  if defined?(::Dalli)
@@ -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
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
  require "timecop"
3
5
 
@@ -123,11 +125,11 @@ describe "#throttle" do
123
125
  notification_data = nil
124
126
  notification_discriminator = nil
125
127
 
126
- ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
127
- notification_matched = request.env["rack.attack.matched"]
128
- notification_type = request.env["rack.attack.match_type"]
129
- notification_data = request.env['rack.attack.match_data']
130
- notification_discriminator = request.env['rack.attack.match_discriminator']
128
+ ActiveSupport::Notifications.subscribe("throttle.rack_attack") do |_name, _start, _finish, _id, payload|
129
+ notification_matched = payload[:request].env["rack.attack.matched"]
130
+ notification_type = payload[:request].env["rack.attack.match_type"]
131
+ notification_data = payload[:request].env['rack.attack.match_data']
132
+ notification_discriminator = payload[:request].env['rack.attack.match_discriminator']
131
133
  end
132
134
 
133
135
  get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
 
3
5
  describe "#track" do
@@ -9,9 +11,9 @@ describe "#track" do
9
11
  notification_matched = nil
10
12
  notification_type = nil
11
13
 
12
- ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
13
- notification_matched = request.env["rack.attack.matched"]
14
- notification_type = request.env["rack.attack.match_type"]
14
+ ActiveSupport::Notifications.subscribe("track.rack_attack") do |_name, _start, _finish, _id, payload|
15
+ notification_matched = payload[:request].env["rack.attack.matched"]
16
+ notification_type = payload[:request].env["rack.attack.match_type"]
15
17
  end
16
18
 
17
19
  get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
  require "timecop"
3
5
 
@@ -12,9 +14,9 @@ describe "#track with throttle-ish options" do
12
14
  notification_matched = nil
13
15
  notification_type = nil
14
16
 
15
- ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
16
- notification_matched = request.env["rack.attack.matched"]
17
- notification_type = request.env["rack.attack.match_type"]
17
+ ActiveSupport::Notifications.subscribe("track.rack_attack") do |_name, _start, _finish, _id, payload|
18
+ notification_matched = payload[:request].env["rack.attack.matched"]
19
+ notification_type = payload[:request].env["rack.attack.match_type"]
18
20
  end
19
21
 
20
22
  get "/", {}, "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 'Rack::Attack.Allow2Ban' do
@@ -7,7 +9,7 @@ describe 'Rack::Attack.Allow2Ban' do
7
9
  @findtime = 60
8
10
  @bantime = 60
9
11
  Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
10
- @f2b_options = { :bantime => @bantime, :findtime => @findtime, :maxretry => 2 }
12
+ @f2b_options = { bantime: @bantime, findtime: @findtime, maxretry: 2 }
11
13
 
12
14
  Rack::Attack.blocklist('pentest') do |req|
13
15
  Rack::Attack::Allow2Ban.filter(req.ip, @f2b_options) { req.query_string =~ /OMGHAX/ }
@@ -18,7 +20,8 @@ describe 'Rack::Attack.Allow2Ban' do
18
20
  describe 'making ok request' do
19
21
  it 'succeeds' do
20
22
  get '/', {}, 'REMOTE_ADDR' => '1.2.3.4'
21
- last_response.status.must_equal 200
23
+
24
+ _(last_response.status).must_equal 200
22
25
  end
23
26
  end
24
27
 
@@ -27,17 +30,18 @@ describe 'Rack::Attack.Allow2Ban' do
27
30
  before { get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' }
28
31
 
29
32
  it 'succeeds' do
30
- last_response.status.must_equal 200
33
+ _(last_response.status).must_equal 200
31
34
  end
32
35
 
33
36
  it 'increases fail count' do
34
37
  key = "rack::attack:#{Time.now.to_i / @findtime}:allow2ban:count:1.2.3.4"
35
- @cache.store.read(key).must_equal 1
38
+
39
+ _(@cache.store.read(key)).must_equal 1
36
40
  end
37
41
 
38
42
  it 'is not banned' do
39
43
  key = "rack::attack:allow2ban:1.2.3.4"
40
- @cache.store.read(key).must_be_nil
44
+ _(@cache.store.read(key)).must_be_nil
41
45
  end
42
46
  end
43
47
 
@@ -49,17 +53,17 @@ describe 'Rack::Attack.Allow2Ban' do
49
53
  end
50
54
 
51
55
  it 'succeeds' do
52
- last_response.status.must_equal 200
56
+ _(last_response.status).must_equal 200
53
57
  end
54
58
 
55
59
  it 'increases fail count' do
56
60
  key = "rack::attack:#{Time.now.to_i / @findtime}:allow2ban:count:1.2.3.4"
57
- @cache.store.read(key).must_equal 2
61
+ _(@cache.store.read(key)).must_equal 2
58
62
  end
59
63
 
60
64
  it 'is banned' do
61
65
  key = "rack::attack:allow2ban:ban:1.2.3.4"
62
- @cache.store.read(key).must_equal 1
66
+ _(@cache.store.read(key)).must_equal 1
63
67
  end
64
68
  end
65
69
  end
@@ -75,7 +79,8 @@ describe 'Rack::Attack.Allow2Ban' do
75
79
  describe 'making request for other discriminator' do
76
80
  it 'succeeds' do
77
81
  get '/', {}, 'REMOTE_ADDR' => '2.2.3.4'
78
- last_response.status.must_equal 200
82
+
83
+ _(last_response.status).must_equal 200
79
84
  end
80
85
  end
81
86
 
@@ -85,17 +90,17 @@ describe 'Rack::Attack.Allow2Ban' do
85
90
  end
86
91
 
87
92
  it 'fails' do
88
- last_response.status.must_equal 403
93
+ _(last_response.status).must_equal 403
89
94
  end
90
95
 
91
96
  it 'does not increase fail count' do
92
97
  key = "rack::attack:#{Time.now.to_i / @findtime}:allow2ban:count:1.2.3.4"
93
- @cache.store.read(key).must_equal 2
98
+ _(@cache.store.read(key)).must_equal 2
94
99
  end
95
100
 
96
101
  it 'is still banned' do
97
102
  key = "rack::attack:allow2ban:ban:1.2.3.4"
98
- @cache.store.read(key).must_equal 1
103
+ _(@cache.store.read(key)).must_equal 1
99
104
  end
100
105
  end
101
106
 
@@ -105,17 +110,17 @@ describe 'Rack::Attack.Allow2Ban' do
105
110
  end
106
111
 
107
112
  it 'fails' do
108
- last_response.status.must_equal 403
113
+ _(last_response.status).must_equal 403
109
114
  end
110
115
 
111
116
  it 'does not increase fail count' do
112
117
  key = "rack::attack:#{Time.now.to_i / @findtime}:allow2ban:count:1.2.3.4"
113
- @cache.store.read(key).must_equal 2
118
+ _(@cache.store.read(key)).must_equal 2
114
119
  end
115
120
 
116
121
  it 'is still banned' do
117
122
  key = "rack::attack:allow2ban:ban:1.2.3.4"
118
- @cache.store.read(key).must_equal 1
123
+ _(@cache.store.read(key)).must_equal 1
119
124
  end
120
125
  end
121
126
  end