rack-attack 5.4.2 → 6.0.0

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