rack-attack 5.4.2 → 6.0.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +65 -23
  3. data/Rakefile +3 -1
  4. data/lib/rack/attack.rb +46 -70
  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 +5 -3
  8. data/lib/rack/attack/check.rb +3 -1
  9. data/lib/rack/attack/fail2ban.rb +2 -0
  10. data/lib/rack/attack/path_normalizer.rb +2 -0
  11. data/lib/rack/attack/request.rb +2 -0
  12. data/lib/rack/attack/safelist.rb +3 -1
  13. data/lib/rack/attack/store_proxy.rb +12 -14
  14. data/lib/rack/attack/store_proxy/active_support_redis_store_proxy.rb +37 -0
  15. data/lib/rack/attack/store_proxy/dalli_proxy.rb +27 -13
  16. data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +2 -4
  17. data/lib/rack/attack/store_proxy/redis_proxy.rb +16 -10
  18. data/lib/rack/attack/store_proxy/redis_store_proxy.rb +5 -5
  19. data/lib/rack/attack/throttle.rb +8 -6
  20. data/lib/rack/attack/track.rb +5 -3
  21. data/lib/rack/attack/version.rb +3 -1
  22. data/spec/acceptance/allow2ban_spec.rb +2 -0
  23. data/spec/acceptance/blocking_ip_spec.rb +4 -2
  24. data/spec/acceptance/blocking_spec.rb +45 -3
  25. data/spec/acceptance/blocking_subnet_spec.rb +4 -2
  26. data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +8 -12
  27. data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +8 -12
  28. data/spec/acceptance/cache_store_config_for_throttle_spec.rb +2 -0
  29. data/spec/acceptance/cache_store_config_with_rails_spec.rb +2 -0
  30. data/spec/acceptance/customizing_blocked_response_spec.rb +2 -0
  31. data/spec/acceptance/customizing_throttled_response_spec.rb +2 -0
  32. data/spec/acceptance/extending_request_object_spec.rb +2 -0
  33. data/spec/acceptance/fail2ban_spec.rb +2 -0
  34. data/spec/acceptance/safelisting_ip_spec.rb +4 -2
  35. data/spec/acceptance/safelisting_spec.rb +57 -3
  36. data/spec/acceptance/safelisting_subnet_spec.rb +4 -2
  37. data/spec/acceptance/stores/active_support_dalli_store_spec.rb +2 -0
  38. data/spec/acceptance/stores/active_support_mem_cache_store_spec.rb +2 -0
  39. data/spec/acceptance/stores/active_support_memory_store_spec.rb +2 -0
  40. data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +2 -0
  41. data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +2 -0
  42. data/spec/acceptance/stores/active_support_redis_store_spec.rb +3 -1
  43. data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +2 -0
  44. data/spec/acceptance/stores/dalli_client_spec.rb +2 -0
  45. data/spec/acceptance/stores/redis_store_spec.rb +2 -0
  46. data/spec/acceptance/throttling_spec.rb +7 -5
  47. data/spec/acceptance/track_spec.rb +5 -3
  48. data/spec/acceptance/track_throttle_spec.rb +5 -3
  49. data/spec/allow2ban_spec.rb +3 -1
  50. data/spec/fail2ban_spec.rb +3 -1
  51. data/spec/integration/offline_spec.rb +3 -1
  52. data/spec/rack_attack_dalli_proxy_spec.rb +2 -0
  53. data/spec/rack_attack_instrumentation_spec.rb +42 -0
  54. data/spec/rack_attack_path_normalizer_spec.rb +2 -0
  55. data/spec/rack_attack_request_spec.rb +2 -0
  56. data/spec/rack_attack_spec.rb +2 -21
  57. data/spec/rack_attack_throttle_spec.rb +10 -8
  58. data/spec/rack_attack_track_spec.rb +4 -2
  59. data/spec/spec_helper.rb +5 -4
  60. data/spec/support/cache_store_helper.rb +2 -0
  61. metadata +21 -14
  62. 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
  require "minitest/stub_const"
3
5
 
@@ -20,11 +22,9 @@ describe "Cache store config when using fail2ban" do
20
22
  raised_exception = nil
21
23
 
22
24
  fake_store_class = Class.new do
23
- def write(key, value)
24
- end
25
+ def write(key, value); end
25
26
 
26
- def increment(key, count, options = {})
27
- end
27
+ def increment(key, count, options = {}); end
28
28
  end
29
29
 
30
30
  Object.stub_const(:FakeStore, fake_store_class) do
@@ -42,11 +42,9 @@ describe "Cache store config when using fail2ban" do
42
42
  raised_exception = nil
43
43
 
44
44
  fake_store_class = Class.new do
45
- def read(key)
46
- end
45
+ def read(key); end
47
46
 
48
- def increment(key, count, options = {})
49
- end
47
+ def increment(key, count, options = {}); end
50
48
  end
51
49
 
52
50
  Object.stub_const(:FakeStore, fake_store_class) do
@@ -64,11 +62,9 @@ describe "Cache store config when using fail2ban" do
64
62
  raised_exception = nil
65
63
 
66
64
  fake_store_class = Class.new do
67
- def read(key)
68
- end
65
+ def read(key); end
69
66
 
70
- def write(key, value)
71
- end
67
+ def write(key, value); end
72
68
  end
73
69
 
74
70
  Object.stub_const(:FakeStore, fake_store_class) do
@@ -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
 
@@ -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)
@@ -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
 
3
5
  if defined?(::ConnectionPool) && defined?(::Redis) && Gem::Version.new(::Redis::VERSION) >= Gem::Version.new("4") && defined?(::ActiveSupport::Cache::RedisCacheStore)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../../spec_helper"
2
4
 
3
5
  if defined?(::Redis) && Gem::Version.new(::Redis::VERSION) >= Gem::Version.new("4") && defined?(::ActiveSupport::Cache::RedisCacheStore)
@@ -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)
@@ -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/ }
@@ -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/ }
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/cache'
2
4
  require_relative '../spec_helper'
3
5
 
@@ -22,7 +24,7 @@ if defined?(::ActiveSupport::Cache::RedisStore)
22
24
  before do
23
25
  @cache = Rack::Attack::Cache.new
24
26
  # Use presumably unused port for Redis client
25
- @cache.store = ActiveSupport::Cache::RedisStore.new(:host => '127.0.0.1', :port => 3333)
27
+ @cache.store = ActiveSupport::Cache::RedisStore.new(host: '127.0.0.1', port: 3333)
26
28
  end
27
29
  end
28
30
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'spec_helper'
2
4
 
3
5
  describe Rack::Attack::StoreProxy::DalliProxy do
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "spec_helper"
4
+
5
+ # ActiveSupport::Subscribers added in ~> 4.0.2.0
6
+ if ActiveSupport::VERSION::MAJOR > 3
7
+ require_relative 'spec_helper'
8
+ require 'active_support/subscriber'
9
+ class CustomSubscriber < ActiveSupport::Subscriber
10
+ @notification_count = 0
11
+
12
+ class << self
13
+ attr_accessor :notification_count
14
+ end
15
+
16
+ def throttle(_event)
17
+ self.class.notification_count += 1
18
+ end
19
+ end
20
+
21
+ describe 'Rack::Attack.instrument' do
22
+ before do
23
+ @period = 60 # Use a long period; failures due to cache key rotation less likely
24
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
25
+ Rack::Attack.throttle('ip/sec', limit: 1, period: @period) { |req| req.ip }
26
+ end
27
+
28
+ describe "with throttling" do
29
+ before do
30
+ ActiveSupport::Notifications.stub(:notifier, ActiveSupport::Notifications::Fanout.new) do
31
+ CustomSubscriber.attach_to("rack_attack")
32
+ 2.times { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
33
+ end
34
+ end
35
+
36
+ it 'should instrument without error' do
37
+ last_response.status.must_equal 429
38
+ assert_equal 1, CustomSubscriber.notification_count
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'spec_helper'
2
4
 
3
5
  describe Rack::Attack::PathNormalizer do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'spec_helper'
2
4
 
3
5
  describe 'Rack::Attack' do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'spec_helper'
2
4
 
3
5
  describe 'Rack::Attack' do
@@ -24,13 +26,6 @@ describe 'Rack::Attack' do
24
26
  Rack::Attack.blocklists.key?("ip #{@bad_ip}").must_equal true
25
27
  }
26
28
 
27
- it('has a blacklist with a deprication warning') {
28
- _, stderror = capture_io do
29
- Rack::Attack.blacklists.key?("ip #{@bad_ip}").must_equal true
30
- end
31
- assert_match "[DEPRECATION] 'Rack::Attack.blacklists' is deprecated. Please use 'blocklists' instead.", stderror
32
- }
33
-
34
29
  describe "a bad request" do
35
30
  before { get '/', {}, 'REMOTE_ADDR' => @bad_ip }
36
31
 
@@ -55,13 +50,6 @@ describe 'Rack::Attack' do
55
50
 
56
51
  it('has a safelist') { Rack::Attack.safelists.key?("good ua") }
57
52
 
58
- it('has a whitelist with a deprication warning') {
59
- _, stderror = capture_io do
60
- Rack::Attack.whitelists.key?("good ua")
61
- end
62
- assert_match "[DEPRECATION] 'Rack::Attack.whitelists' is deprecated. Please use 'safelists' instead.", stderror
63
- }
64
-
65
53
  describe "with a request match both safelist & blocklist" do
66
54
  before { get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua }
67
55
 
@@ -80,13 +68,6 @@ describe 'Rack::Attack' do
80
68
  it 'should exist' do
81
69
  Rack::Attack.blocklisted_response.must_respond_to :call
82
70
  end
83
-
84
- it 'should give a deprication warning for blacklisted_response' do
85
- _, stderror = capture_io do
86
- Rack::Attack.blacklisted_response
87
- end
88
- assert_match "[DEPRECATION] 'Rack::Attack.blacklisted_response' is deprecated. Please use 'blocklisted_response' instead.", stderror
89
- end
90
71
  end
91
72
 
92
73
  describe '#throttled_response' do