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
  if defined?(::Dalli) && defined?(::ConnectionPool)
@@ -15,28 +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
19
-
20
- it "doesn't leak keys" do
21
- Rack::Attack.throttle("by ip", limit: 1, period: 1) do |request|
22
- request.ip
23
- end
24
-
25
- key = nil
26
-
27
- # Freeze time during these statement to be sure that the key used by rack attack is the same
28
- # we pre-calculate in local variable `key`
29
- Timecop.freeze do
30
- key = "rack::attack:#{Time.now.to_i}:by ip:1.2.3.4"
31
-
32
- get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
33
- end
34
-
35
- assert(Rack::Attack.cache.store.with { |client| client.fetch(key) })
36
-
37
- sleep 2.1
38
-
39
- assert_nil(Rack::Attack.cache.store.with { |client| client.fetch(key) })
40
- end
20
+ it_works_for_cache_backed_features(
21
+ fetch_from_store: ->(key) { Rack::Attack.cache.store.with { |client| client.fetch(key) } }
22
+ )
41
23
  end
42
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)
@@ -14,28 +16,6 @@ if defined?(::Dalli)
14
16
  Rack::Attack.cache.store.flush_all
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
@@ -15,28 +15,6 @@ if defined?(::Redis)
15
15
  Rack::Attack.cache.store.flushdb
16
16
  end
17
17
 
18
- it_works_for_cache_backed_features
19
-
20
- it "doesn't leak keys" do
21
- Rack::Attack.throttle("by ip", limit: 1, period: 1) do |request|
22
- request.ip
23
- end
24
-
25
- key = nil
26
-
27
- # Freeze time during these statement to be sure that the key used by rack attack is the same
28
- # we pre-calculate in local variable `key`
29
- Timecop.freeze do
30
- key = "rack::attack:#{Time.now.to_i}:by ip:1.2.3.4"
31
-
32
- get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
33
- end
34
-
35
- assert Rack::Attack.cache.store.get(key)
36
-
37
- sleep 2.1
38
-
39
- assert_nil Rack::Attack.cache.store.get(key)
40
- end
18
+ it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.get(key) })
41
19
  end
42
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
 
@@ -13,28 +15,6 @@ if defined?(::Redis::Store)
13
15
  Rack::Attack.cache.store.flushdb
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
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'spec_helper'
2
4
 
3
5
  describe 'Rack::Attack.Fail2Ban' do
@@ -7,7 +9,7 @@ describe 'Rack::Attack.Fail2Ban' 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::Fail2Ban.filter(req.ip, @f2b_options) { req.query_string =~ /OMGHAX/ }
@@ -18,7 +20,7 @@ describe 'Rack::Attack.Fail2Ban' 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
+ _(last_response.status).must_equal 200
22
24
  end
23
25
  end
24
26
 
@@ -27,17 +29,17 @@ describe 'Rack::Attack.Fail2Ban' do
27
29
  before { get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' }
28
30
 
29
31
  it 'fails' do
30
- last_response.status.must_equal 403
32
+ _(last_response.status).must_equal 403
31
33
  end
32
34
 
33
35
  it 'increases fail count' do
34
36
  key = "rack::attack:#{Time.now.to_i / @findtime}:fail2ban:count:1.2.3.4"
35
- @cache.store.read(key).must_equal 1
37
+ _(@cache.store.read(key)).must_equal 1
36
38
  end
37
39
 
38
40
  it 'is not banned' do
39
41
  key = "rack::attack:fail2ban:1.2.3.4"
40
- @cache.store.read(key).must_be_nil
42
+ _(@cache.store.read(key)).must_be_nil
41
43
  end
42
44
  end
43
45
 
@@ -49,17 +51,17 @@ describe 'Rack::Attack.Fail2Ban' do
49
51
  end
50
52
 
51
53
  it 'fails' do
52
- last_response.status.must_equal 403
54
+ _(last_response.status).must_equal 403
53
55
  end
54
56
 
55
57
  it 'increases fail count' do
56
58
  key = "rack::attack:#{Time.now.to_i / @findtime}:fail2ban:count:1.2.3.4"
57
- @cache.store.read(key).must_equal 2
59
+ _(@cache.store.read(key)).must_equal 2
58
60
  end
59
61
 
60
62
  it 'is banned' do
61
63
  key = "rack::attack:fail2ban:ban:1.2.3.4"
62
- @cache.store.read(key).must_equal 1
64
+ _(@cache.store.read(key)).must_equal 1
63
65
  end
64
66
  end
65
67
 
@@ -71,7 +73,7 @@ describe 'Rack::Attack.Fail2Ban' do
71
73
  end
72
74
 
73
75
  it 'succeeds' do
74
- last_response.status.must_equal 200
76
+ _(last_response.status).must_equal 200
75
77
  end
76
78
 
77
79
  it 'resets fail count' do
@@ -80,7 +82,7 @@ describe 'Rack::Attack.Fail2Ban' do
80
82
  end
81
83
 
82
84
  it 'IP is not banned' do
83
- Rack::Attack::Fail2Ban.banned?('1.2.3.4').must_equal false
85
+ _(Rack::Attack::Fail2Ban.banned?('1.2.3.4')).must_equal false
84
86
  end
85
87
  end
86
88
  end
@@ -96,7 +98,8 @@ describe 'Rack::Attack.Fail2Ban' do
96
98
  describe 'making request for other discriminator' do
97
99
  it 'succeeds' do
98
100
  get '/', {}, 'REMOTE_ADDR' => '2.2.3.4'
99
- last_response.status.must_equal 200
101
+
102
+ _(last_response.status).must_equal 200
100
103
  end
101
104
  end
102
105
 
@@ -106,17 +109,17 @@ describe 'Rack::Attack.Fail2Ban' do
106
109
  end
107
110
 
108
111
  it 'fails' do
109
- last_response.status.must_equal 403
112
+ _(last_response.status).must_equal 403
110
113
  end
111
114
 
112
115
  it 'does not increase fail count' do
113
116
  key = "rack::attack:#{Time.now.to_i / @findtime}:fail2ban:count:1.2.3.4"
114
- @cache.store.read(key).must_equal 2
117
+ _(@cache.store.read(key)).must_equal 2
115
118
  end
116
119
 
117
120
  it 'is still banned' do
118
121
  key = "rack::attack:fail2ban:ban:1.2.3.4"
119
- @cache.store.read(key).must_equal 1
122
+ _(@cache.store.read(key)).must_equal 1
120
123
  end
121
124
  end
122
125
 
@@ -126,17 +129,17 @@ describe 'Rack::Attack.Fail2Ban' do
126
129
  end
127
130
 
128
131
  it 'fails' do
129
- last_response.status.must_equal 403
132
+ _(last_response.status).must_equal 403
130
133
  end
131
134
 
132
135
  it 'does not increase fail count' do
133
136
  key = "rack::attack:#{Time.now.to_i / @findtime}:fail2ban:count:1.2.3.4"
134
- @cache.store.read(key).must_equal 2
137
+ _(@cache.store.read(key)).must_equal 2
135
138
  end
136
139
 
137
140
  it 'is still banned' do
138
141
  key = "rack::attack:fail2ban:ban:1.2.3.4"
139
- @cache.store.read(key).must_equal 1
142
+ _(@cache.store.read(key)).must_equal 1
140
143
  end
141
144
  end
142
145
  end