rack-attack 5.4.1 → 6.2.1

Sign up to get free protection for your applications and to get access to all the features.
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