rack-attack 6.6.1 → 6.8.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.
- checksums.yaml +4 -4
- data/README.md +12 -8
- data/lib/rack/attack/base_proxy.rb +1 -0
- data/lib/rack/attack/cache.rb +10 -4
- data/lib/rack/attack/configuration.rb +7 -3
- data/lib/rack/attack/path_normalizer.rb +3 -1
- data/lib/rack/attack/railtie.rb +6 -0
- data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +16 -10
- data/lib/rack/attack/store_proxy/redis_proxy.rb +2 -1
- data/lib/rack/attack/throttle.rb +2 -1
- data/lib/rack/attack/version.rb +1 -1
- data/lib/rack/attack.rb +3 -1
- data/spec/acceptance/blocking_ip_spec.rb +13 -8
- data/spec/acceptance/blocking_spec.rb +16 -18
- data/spec/acceptance/blocking_subnet_spec.rb +7 -8
- data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +5 -3
- data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +7 -5
- data/spec/acceptance/cache_store_config_for_throttle_spec.rb +5 -3
- data/spec/acceptance/cache_store_config_with_rails_spec.rb +6 -4
- data/spec/acceptance/extending_request_object_spec.rb +3 -7
- data/spec/acceptance/fail2ban_spec.rb +42 -0
- data/spec/acceptance/rails_middleware_spec.rb +1 -1
- data/spec/acceptance/safelisting_ip_spec.rb +12 -4
- data/spec/acceptance/safelisting_spec.rb +14 -14
- data/spec/acceptance/safelisting_subnet_spec.rb +6 -4
- data/spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb +5 -2
- data/spec/acceptance/stores/active_support_mem_cache_store_spec.rb +0 -1
- data/spec/acceptance/stores/active_support_memory_store_spec.rb +0 -2
- data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +5 -2
- data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +0 -1
- data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +0 -1
- data/spec/acceptance/stores/dalli_client_spec.rb +0 -1
- data/spec/acceptance/stores/redis_spec.rb +0 -1
- data/spec/acceptance/stores/redis_store_spec.rb +1 -3
- data/spec/acceptance/throttling_spec.rb +14 -23
- data/spec/acceptance/track_spec.rb +8 -9
- data/spec/acceptance/track_throttle_spec.rb +10 -16
- data/spec/configuration_spec.rb +33 -0
- data/spec/integration/offline_spec.rb +0 -12
- data/spec/rack_attack_instrumentation_spec.rb +25 -28
- data/spec/rack_attack_request_spec.rb +2 -4
- data/spec/rack_attack_reset_spec.rb +90 -0
- data/spec/rack_attack_spec.rb +4 -22
- data/spec/rack_attack_throttle_spec.rb +49 -28
- data/spec/rack_attack_track_spec.rb +4 -17
- data/spec/spec_helper.rb +7 -5
- data/spec/support/cache_store_helper.rb +31 -25
- data/spec/support/freeze_time_helper.rb +9 -0
- metadata +48 -28
- data/lib/rack/attack/store_proxy/active_support_redis_store_proxy.rb +0 -39
- data/spec/acceptance/stores/active_support_dalli_store_spec.rb +0 -25
- data/spec/acceptance/stores/active_support_redis_store_spec.rb +0 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cca196b4f54fee6e576f7afd081dcc0311c25f2a9705a498bf5c319ec27a0d79
|
4
|
+
data.tar.gz: c758c6d6c9a10eac5ae1b20b1d501c3f9ded73d1c2dd77777a57a1727addf0fa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4f8a0c70cb9744d842786b4960d9ec82f971c54a2fa4c036c44919be5695310cb192f22fa5910edd187f1655e537e894f092db0b16bda41744ecac2cde9e1bb3
|
7
|
+
data.tar.gz: 6e999c3c981c90130aac0bffddd69feb09ff480cd1b791dea33f580080a2cf7ffc4e4949c93ec2e4dfe61dd1cdd957291e7893c0ca2b879b2092ac6984a45169
|
data/README.md
CHANGED
@@ -11,7 +11,6 @@ See the [Backing & Hacking blog post](https://www.kickstarter.com/backing-and-ha
|
|
11
11
|
|
12
12
|
[](https://badge.fury.io/rb/rack-attack)
|
13
13
|
[](https://github.com/rack/rack-attack/actions/workflows/build.yml)
|
14
|
-
[](https://codeclimate.com/github/kickstarter/rack-attack)
|
15
14
|
[](https://gitter.im/rack-attack/rack-attack)
|
16
15
|
|
17
16
|
## Table of contents
|
@@ -56,7 +55,7 @@ Add this line to your application's Gemfile:
|
|
56
55
|
```ruby
|
57
56
|
# In your Gemfile
|
58
57
|
|
59
|
-
gem
|
58
|
+
gem "rack-attack", "~> 6.8"
|
60
59
|
```
|
61
60
|
|
62
61
|
And then execute:
|
@@ -291,7 +290,7 @@ Rack::Attack.track("special_agent", limit: 6, period: 60) do |req|
|
|
291
290
|
end
|
292
291
|
|
293
292
|
# Track it using ActiveSupport::Notification
|
294
|
-
ActiveSupport::Notifications.subscribe("track.rack_attack") do |name, start, finish,
|
293
|
+
ActiveSupport::Notifications.subscribe("track.rack_attack") do |name, start, finish, instrumenter_id, payload|
|
295
294
|
req = payload[:request]
|
296
295
|
if req.env['rack.attack.matched'] == "special_agent"
|
297
296
|
Rails.logger.info "special_agent: #{req.path}"
|
@@ -302,13 +301,18 @@ end
|
|
302
301
|
|
303
302
|
### Cache store configuration
|
304
303
|
|
305
|
-
Throttle, allow2ban and fail2ban state is stored in a configurable cache (which defaults to `Rails.cache` if present), presumably backed by memcached or redis ([at least gem v3.0.0](https://rubygems.org/gems/redis)).
|
304
|
+
Throttle, track, allow2ban and fail2ban state is stored in a configurable cache (which defaults to `Rails.cache` if present), presumably backed by memcached or redis ([at least gem v3.0.0](https://rubygems.org/gems/redis)).
|
306
305
|
|
307
306
|
```ruby
|
308
|
-
|
307
|
+
# This is the default
|
308
|
+
Rack::Attack.cache.store = Rails.cache
|
309
|
+
# It is recommended to use a separate database for throttling/allow2ban/fail2ban.
|
310
|
+
Rack::Attack.cache.store = ActiveSupport::Cache::RedisCacheStore.new(url: "...")
|
309
311
|
```
|
310
312
|
|
311
|
-
|
313
|
+
Most applications should use a new, separate database used only for `rack-attack`. During an actual attack or periods of heavy load, this database will come under heavy load. Keeping it on a separate database instance will give you additional resilience and make sure that other functions (like caching for your application) don't go down.
|
314
|
+
|
315
|
+
Note that `Rack::Attack.cache` is only used for throttling, allow2ban and fail2ban filtering; not blocklisting and safelisting. Your cache store must implement `increment` and `write` like [ActiveSupport::Cache::Store](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html). This means that other cache stores which inherit from ActiveSupport::Cache::Store are also compatible. In-memory stores which are not backed by an external database, such as `ActiveSupport::Cache::MemoryStore.new`, will be mostly ineffective because each Ruby process in your deployment will have it's own state, effectively multiplying the number of requests each client can make by the number of Ruby processes you have deployed.
|
312
316
|
|
313
317
|
## Customizing responses
|
314
318
|
|
@@ -378,7 +382,7 @@ To get notified about specific type of events, subscribe to the event name follo
|
|
378
382
|
E.g. for throttles use:
|
379
383
|
|
380
384
|
```ruby
|
381
|
-
ActiveSupport::Notifications.subscribe("throttle.rack_attack") do |name, start, finish,
|
385
|
+
ActiveSupport::Notifications.subscribe("throttle.rack_attack") do |name, start, finish, instrumenter_id, payload|
|
382
386
|
# request object available in payload[:request]
|
383
387
|
|
384
388
|
# Your code here
|
@@ -388,7 +392,7 @@ end
|
|
388
392
|
If you want to subscribe to every `rack_attack` event, use:
|
389
393
|
|
390
394
|
```ruby
|
391
|
-
ActiveSupport::Notifications.subscribe(/rack_attack/) do |name, start, finish,
|
395
|
+
ActiveSupport::Notifications.subscribe(/rack_attack/) do |name, start, finish, instrumenter_id, payload|
|
392
396
|
# request object available in payload[:request]
|
393
397
|
|
394
398
|
# Your code here
|
data/lib/rack/attack/cache.rb
CHANGED
@@ -6,8 +6,14 @@ module Rack
|
|
6
6
|
attr_accessor :prefix
|
7
7
|
attr_reader :last_epoch_time
|
8
8
|
|
9
|
-
def
|
10
|
-
|
9
|
+
def self.default_store
|
10
|
+
if Object.const_defined?(:Rails) && Rails.respond_to?(:cache)
|
11
|
+
::Rails.cache
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(store: self.class.default_store)
|
16
|
+
self.store = store
|
11
17
|
@prefix = 'rack::attack'
|
12
18
|
end
|
13
19
|
|
@@ -49,7 +55,7 @@ module Rack
|
|
49
55
|
|
50
56
|
def reset!
|
51
57
|
if store.respond_to?(:delete_matched)
|
52
|
-
store.delete_matched(
|
58
|
+
store.delete_matched(/#{prefix}*/)
|
53
59
|
else
|
54
60
|
raise(
|
55
61
|
Rack::Attack::IncompatibleStoreError,
|
@@ -62,7 +68,7 @@ module Rack
|
|
62
68
|
|
63
69
|
def key_and_expiry(unprefixed_key, period)
|
64
70
|
@last_epoch_time = Time.now.to_i
|
65
|
-
# Add 1 to expires_in to avoid timing error: https://
|
71
|
+
# Add 1 to expires_in to avoid timing error: https://github.com/rack/rack-attack/pull/85
|
66
72
|
expires_in = (period - (@last_epoch_time % period) + 1).to_i
|
67
73
|
["#{prefix}:#{(@last_epoch_time / period).to_i}:#{unprefixed_key}", expires_in]
|
68
74
|
end
|
@@ -19,7 +19,7 @@ module Rack
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
-
attr_reader :safelists, :blocklists, :throttles, :anonymous_blocklists, :anonymous_safelists
|
22
|
+
attr_reader :safelists, :blocklists, :throttles, :tracks, :anonymous_blocklists, :anonymous_safelists
|
23
23
|
attr_accessor :blocklisted_responder, :throttled_responder, :throttled_response_retry_after_header
|
24
24
|
|
25
25
|
attr_reader :blocklisted_response, :throttled_response # Keeping these for backwards compatibility
|
@@ -61,11 +61,15 @@ module Rack
|
|
61
61
|
end
|
62
62
|
|
63
63
|
def blocklist_ip(ip_address)
|
64
|
-
@anonymous_blocklists << Blocklist.new
|
64
|
+
@anonymous_blocklists << Blocklist.new do |request|
|
65
|
+
request.ip && !request.ip.empty? && IPAddr.new(ip_address).include?(IPAddr.new(request.ip))
|
66
|
+
end
|
65
67
|
end
|
66
68
|
|
67
69
|
def safelist_ip(ip_address)
|
68
|
-
@anonymous_safelists << Safelist.new
|
70
|
+
@anonymous_safelists << Safelist.new do |request|
|
71
|
+
request.ip && !request.ip.empty? && IPAddr.new(ip_address).include?(IPAddr.new(request.ip))
|
72
|
+
end
|
69
73
|
end
|
70
74
|
|
71
75
|
def throttle(name, options, &block)
|
@@ -4,7 +4,9 @@ module Rack
|
|
4
4
|
class Attack
|
5
5
|
# When using Rack::Attack with a Rails app, developers expect the request path
|
6
6
|
# to be normalized. In particular, trailing slashes are stripped.
|
7
|
-
# (See
|
7
|
+
# (See
|
8
|
+
# https://github.com/rails/rails/blob/f8edd20/actionpack/lib/action_dispatch/journey/router/utils.rb#L5-L22
|
9
|
+
# for implementation.)
|
8
10
|
#
|
9
11
|
# Look for an ActionDispatch utility class that Rails folks would expect
|
10
12
|
# to normalize request paths. If unavailable, use a fallback class that
|
data/lib/rack/attack/railtie.rb
CHANGED
@@ -10,17 +10,19 @@ module Rack
|
|
10
10
|
store.class.name == "ActiveSupport::Cache::RedisCacheStore"
|
11
11
|
end
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
13
|
+
if defined?(::ActiveSupport) && ::ActiveSupport::VERSION::MAJOR < 6
|
14
|
+
def increment(name, amount = 1, **options)
|
15
|
+
# RedisCacheStore#increment ignores options[:expires_in] in versions prior to 6.
|
16
|
+
#
|
17
|
+
# So in order to workaround this we use RedisCacheStore#write (which sets expiration) to initialize
|
18
|
+
# the counter. After that we continue using the original RedisCacheStore#increment.
|
19
|
+
if options[:expires_in] && !read(name)
|
20
|
+
write(name, amount, options)
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
22
|
+
amount
|
23
|
+
else
|
24
|
+
super
|
25
|
+
end
|
24
26
|
end
|
25
27
|
end
|
26
28
|
|
@@ -31,6 +33,10 @@ module Rack
|
|
31
33
|
def write(name, value, options = {})
|
32
34
|
super(name, value, options.merge!(raw: true))
|
33
35
|
end
|
36
|
+
|
37
|
+
def delete_matched(matcher, options = nil)
|
38
|
+
super(matcher.source, options)
|
39
|
+
end
|
34
40
|
end
|
35
41
|
end
|
36
42
|
end
|
@@ -45,11 +45,12 @@ module Rack
|
|
45
45
|
|
46
46
|
def delete_matched(matcher, _options = nil)
|
47
47
|
cursor = "0"
|
48
|
+
source = matcher.source
|
48
49
|
|
49
50
|
rescuing do
|
50
51
|
# Fetch keys in batches using SCAN to avoid blocking the Redis server.
|
51
52
|
loop do
|
52
|
-
cursor, keys = scan(cursor, match:
|
53
|
+
cursor, keys = scan(cursor, match: source, count: 1000)
|
53
54
|
del(*keys) unless keys.empty?
|
54
55
|
break if cursor == "0"
|
55
56
|
end
|
data/lib/rack/attack/throttle.rb
CHANGED
@@ -38,8 +38,9 @@ module Rack
|
|
38
38
|
epoch_time: cache.last_epoch_time
|
39
39
|
}
|
40
40
|
|
41
|
+
annotate_request_with_throttle_data(request, data)
|
42
|
+
|
41
43
|
(count > current_limit).tap do |throttled|
|
42
|
-
annotate_request_with_throttle_data(request, data)
|
43
44
|
if throttled
|
44
45
|
annotate_request_with_matched_data(request, data)
|
45
46
|
Rack::Attack.instrument(request)
|
data/lib/rack/attack/version.rb
CHANGED
data/lib/rack/attack.rb
CHANGED
@@ -11,15 +11,17 @@ require 'rack/attack/store_proxy/mem_cache_store_proxy'
|
|
11
11
|
require 'rack/attack/store_proxy/redis_proxy'
|
12
12
|
require 'rack/attack/store_proxy/redis_store_proxy'
|
13
13
|
require 'rack/attack/store_proxy/redis_cache_store_proxy'
|
14
|
-
require 'rack/attack/store_proxy/active_support_redis_store_proxy'
|
15
14
|
|
16
15
|
require 'rack/attack/railtie' if defined?(::Rails)
|
17
16
|
|
18
17
|
module Rack
|
19
18
|
class Attack
|
20
19
|
class Error < StandardError; end
|
20
|
+
|
21
21
|
class MisconfiguredStoreError < Error; end
|
22
|
+
|
22
23
|
class MissingStoreError < Error; end
|
24
|
+
|
23
25
|
class IncompatibleStoreError < Error; end
|
24
26
|
|
25
27
|
autoload :Check, 'rack/attack/check'
|
@@ -3,6 +3,8 @@
|
|
3
3
|
require_relative "../spec_helper"
|
4
4
|
|
5
5
|
describe "Blocking an IP" do
|
6
|
+
let(:notifications) { [] }
|
7
|
+
|
6
8
|
before do
|
7
9
|
Rack::Attack.blocklist_ip("1.2.3.4")
|
8
10
|
end
|
@@ -19,22 +21,25 @@ describe "Blocking an IP" do
|
|
19
21
|
assert_equal 200, last_response.status
|
20
22
|
end
|
21
23
|
|
22
|
-
it "
|
23
|
-
|
24
|
-
|
24
|
+
it "succeeds if IP is missing" do
|
25
|
+
get "/", {}, "REMOTE_ADDR" => ""
|
26
|
+
|
27
|
+
assert_equal 200, last_response.status
|
28
|
+
end
|
25
29
|
|
30
|
+
it "notifies when the request is blocked" do
|
26
31
|
ActiveSupport::Notifications.subscribe("blocklist.rack_attack") do |_name, _start, _finish, _id, payload|
|
27
|
-
|
28
|
-
notification_type = payload[:request].env["rack.attack.match_type"]
|
32
|
+
notifications.push(payload)
|
29
33
|
end
|
30
34
|
|
31
35
|
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
32
36
|
|
33
|
-
|
37
|
+
assert notifications.empty?
|
34
38
|
|
35
39
|
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
36
40
|
|
37
|
-
|
38
|
-
|
41
|
+
assert_equal 1, notifications.size
|
42
|
+
notification = notifications.pop
|
43
|
+
assert_equal :blocklist, notification[:request].env["rack.attack.match_type"]
|
39
44
|
end
|
40
45
|
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
require_relative "../spec_helper"
|
4
4
|
|
5
5
|
describe "#blocklist" do
|
6
|
+
let(:notifications) { [] }
|
7
|
+
|
6
8
|
before do
|
7
9
|
Rack::Attack.blocklist do |request|
|
8
10
|
request.ip == "1.2.3.4"
|
@@ -22,27 +24,26 @@ describe "#blocklist" do
|
|
22
24
|
end
|
23
25
|
|
24
26
|
it "notifies when the request is blocked" do
|
25
|
-
notification_matched = nil
|
26
|
-
notification_type = nil
|
27
|
-
|
28
27
|
ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, payload|
|
29
|
-
|
30
|
-
notification_type = payload[:request].env["rack.attack.match_type"]
|
28
|
+
notifications.push(payload)
|
31
29
|
end
|
32
30
|
|
33
31
|
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
34
32
|
|
35
|
-
|
36
|
-
assert_nil notification_type
|
33
|
+
assert notifications.empty?
|
37
34
|
|
38
35
|
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
39
36
|
|
40
|
-
|
41
|
-
|
37
|
+
assert_equal 1, notifications.size
|
38
|
+
notification = notifications.pop
|
39
|
+
assert_nil notification[:request].env["rack.attack.matched"]
|
40
|
+
assert_equal :blocklist, notification[:request].env["rack.attack.match_type"]
|
42
41
|
end
|
43
42
|
end
|
44
43
|
|
45
44
|
describe "#blocklist with name" do
|
45
|
+
let(:notifications) { [] }
|
46
|
+
|
46
47
|
before do
|
47
48
|
Rack::Attack.blocklist("block 1.2.3.4") do |request|
|
48
49
|
request.ip == "1.2.3.4"
|
@@ -62,22 +63,19 @@ describe "#blocklist with name" do
|
|
62
63
|
end
|
63
64
|
|
64
65
|
it "notifies when the request is blocked" do
|
65
|
-
notification_matched = nil
|
66
|
-
notification_type = nil
|
67
|
-
|
68
66
|
ActiveSupport::Notifications.subscribe("blocklist.rack_attack") do |_name, _start, _finish, _id, payload|
|
69
|
-
|
70
|
-
notification_type = payload[:request].env["rack.attack.match_type"]
|
67
|
+
notifications.push(payload)
|
71
68
|
end
|
72
69
|
|
73
70
|
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
74
71
|
|
75
|
-
|
76
|
-
assert_nil notification_type
|
72
|
+
assert notifications.empty?
|
77
73
|
|
78
74
|
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
79
75
|
|
80
|
-
assert_equal
|
81
|
-
|
76
|
+
assert_equal 1, notifications.size
|
77
|
+
notification = notifications.pop
|
78
|
+
assert_equal "block 1.2.3.4", notification[:request].env["rack.attack.matched"]
|
79
|
+
assert_equal :blocklist, notification[:request].env["rack.attack.match_type"]
|
82
80
|
end
|
83
81
|
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
require_relative "../spec_helper"
|
4
4
|
|
5
5
|
describe "Blocking an IP subnet" do
|
6
|
+
let(:notifications) { [] }
|
7
|
+
|
6
8
|
before do
|
7
9
|
Rack::Attack.blocklist_ip("1.2.3.4/31")
|
8
10
|
end
|
@@ -26,21 +28,18 @@ describe "Blocking an IP subnet" do
|
|
26
28
|
end
|
27
29
|
|
28
30
|
it "notifies when the request is blocked" do
|
29
|
-
notified = false
|
30
|
-
notification_type = nil
|
31
|
-
|
32
31
|
ActiveSupport::Notifications.subscribe("blocklist.rack_attack") do |_name, _start, _finish, _id, payload|
|
33
|
-
|
34
|
-
notification_type = payload[:request].env["rack.attack.match_type"]
|
32
|
+
notifications.push(payload)
|
35
33
|
end
|
36
34
|
|
37
35
|
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
38
36
|
|
39
|
-
|
37
|
+
assert notifications.empty?
|
40
38
|
|
41
39
|
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
42
40
|
|
43
|
-
|
44
|
-
|
41
|
+
assert_equal 1, notifications.size
|
42
|
+
notification = notifications.pop
|
43
|
+
assert_equal :blocklist, notification[:request].env["rack.attack.match_type"]
|
45
44
|
end
|
46
45
|
end
|
@@ -12,9 +12,11 @@ describe "Cache store config when using allow2ban" do
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
unless defined?(Rails)
|
16
|
+
it "gives semantic error if no store was configured" do
|
17
|
+
assert_raises(Rack::Attack::MissingStoreError) do
|
18
|
+
get "/scarce-resource"
|
19
|
+
end
|
18
20
|
end
|
19
21
|
end
|
20
22
|
|
@@ -12,9 +12,11 @@ describe "Cache store config when using fail2ban" do
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
unless defined?(Rails)
|
16
|
+
it "gives semantic error if no store was configured" do
|
17
|
+
assert_raises(Rack::Attack::MissingStoreError) do
|
18
|
+
get "/private-place"
|
19
|
+
end
|
18
20
|
end
|
19
21
|
end
|
20
22
|
|
@@ -79,7 +81,7 @@ describe "Cache store config when using fail2ban" do
|
|
79
81
|
end
|
80
82
|
|
81
83
|
it "works with any object that responds to #read, #write and #increment" do
|
82
|
-
|
84
|
+
fake_store_class = Class.new do
|
83
85
|
attr_accessor :backend
|
84
86
|
|
85
87
|
def initialize
|
@@ -100,7 +102,7 @@ describe "Cache store config when using fail2ban" do
|
|
100
102
|
end
|
101
103
|
end
|
102
104
|
|
103
|
-
Rack::Attack.cache.store =
|
105
|
+
Rack::Attack.cache.store = fake_store_class.new
|
104
106
|
|
105
107
|
get "/"
|
106
108
|
assert_equal 200, last_response.status
|
@@ -9,9 +9,11 @@ describe "Cache store config when throttling without Rails" do
|
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
unless defined?(Rails)
|
13
|
+
it "gives semantic error if no store was configured" do
|
14
|
+
assert_raises(Rack::Attack::MissingStoreError) do
|
15
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
16
|
+
end
|
15
17
|
end
|
16
18
|
end
|
17
19
|
|
@@ -11,10 +11,12 @@ describe "Cache store config with Rails" do
|
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
unless defined?(Rails)
|
15
|
+
it "fails when Rails.cache is not set" do
|
16
|
+
Object.stub_const(:Rails, OpenStruct.new(cache: nil)) do
|
17
|
+
assert_raises(Rack::Attack::MissingStoreError) do
|
18
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
19
|
+
end
|
18
20
|
end
|
19
21
|
end
|
20
22
|
end
|
@@ -4,10 +4,8 @@ require_relative "../spec_helper"
|
|
4
4
|
|
5
5
|
describe "Extending the request object" do
|
6
6
|
before do
|
7
|
-
|
8
|
-
|
9
|
-
env["APIKey"] == "private-secret"
|
10
|
-
end
|
7
|
+
Rack::Attack::Request.define_method :authorized? do
|
8
|
+
env["APIKey"] == "private-secret"
|
11
9
|
end
|
12
10
|
|
13
11
|
Rack::Attack.blocklist("unauthorized requests") do |request|
|
@@ -17,9 +15,7 @@ describe "Extending the request object" do
|
|
17
15
|
|
18
16
|
# We don't want the extension to leak to other test cases
|
19
17
|
after do
|
20
|
-
|
21
|
-
remove_method :authorized?
|
22
|
-
end
|
18
|
+
Rack::Attack::Request.undef_method :authorized?
|
23
19
|
end
|
24
20
|
|
25
21
|
it "forbids request if blocklist condition is true" do
|
@@ -4,6 +4,8 @@ require_relative "../spec_helper"
|
|
4
4
|
require "timecop"
|
5
5
|
|
6
6
|
describe "fail2ban" do
|
7
|
+
let(:notifications) { [] }
|
8
|
+
|
7
9
|
before do
|
8
10
|
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
|
9
11
|
|
@@ -75,4 +77,44 @@ describe "fail2ban" do
|
|
75
77
|
assert_equal 200, last_response.status
|
76
78
|
end
|
77
79
|
end
|
80
|
+
|
81
|
+
it "notifies when the request is blocked" do
|
82
|
+
ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, payload|
|
83
|
+
notifications.push(payload)
|
84
|
+
end
|
85
|
+
|
86
|
+
get "/"
|
87
|
+
|
88
|
+
assert_equal 200, last_response.status
|
89
|
+
assert notifications.empty?
|
90
|
+
|
91
|
+
get "/private-place"
|
92
|
+
|
93
|
+
assert_equal 403, last_response.status
|
94
|
+
assert_equal 1, notifications.size
|
95
|
+
notification = notifications.pop
|
96
|
+
assert_equal 'fail2ban pentesters', notification[:request].env["rack.attack.matched"]
|
97
|
+
assert_equal :blocklist, notification[:request].env["rack.attack.match_type"]
|
98
|
+
|
99
|
+
get "/"
|
100
|
+
|
101
|
+
assert_equal 200, last_response.status
|
102
|
+
assert notifications.empty?
|
103
|
+
|
104
|
+
get "/private-place"
|
105
|
+
|
106
|
+
assert_equal 403, last_response.status
|
107
|
+
assert_equal 1, notifications.size
|
108
|
+
notification = notifications.pop
|
109
|
+
assert_equal 'fail2ban pentesters', notification[:request].env["rack.attack.matched"]
|
110
|
+
assert_equal :blocklist, notification[:request].env["rack.attack.match_type"]
|
111
|
+
|
112
|
+
get "/"
|
113
|
+
|
114
|
+
assert_equal 403, last_response.status
|
115
|
+
assert_equal 1, notifications.size
|
116
|
+
notification = notifications.pop
|
117
|
+
assert_equal 'fail2ban pentesters', notification[:request].env["rack.attack.matched"]
|
118
|
+
assert_equal :blocklist, notification[:request].env["rack.attack.match_type"]
|
119
|
+
end
|
78
120
|
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
require_relative "../spec_helper"
|
4
4
|
|
5
5
|
describe "Safelist an IP" do
|
6
|
+
let(:notifications) { [] }
|
7
|
+
|
6
8
|
before do
|
7
9
|
Rack::Attack.blocklist("admin") do |request|
|
8
10
|
request.path == "/admin"
|
@@ -17,6 +19,12 @@ describe "Safelist an IP" do
|
|
17
19
|
assert_equal 403, last_response.status
|
18
20
|
end
|
19
21
|
|
22
|
+
it "forbids request if blocklist condition is true and safelist is false (missing IP)" do
|
23
|
+
get "/admin", {}, "REMOTE_ADDR" => ""
|
24
|
+
|
25
|
+
assert_equal 403, last_response.status
|
26
|
+
end
|
27
|
+
|
20
28
|
it "succeeds if blocklist condition is false and safelist is false" do
|
21
29
|
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
22
30
|
|
@@ -36,15 +44,15 @@ describe "Safelist an IP" do
|
|
36
44
|
end
|
37
45
|
|
38
46
|
it "notifies when the request is safe" do
|
39
|
-
notification_type = nil
|
40
|
-
|
41
47
|
ActiveSupport::Notifications.subscribe("safelist.rack_attack") do |_name, _start, _finish, _id, payload|
|
42
|
-
|
48
|
+
notifications.push(payload)
|
43
49
|
end
|
44
50
|
|
45
51
|
get "/admin", {}, "REMOTE_ADDR" => "5.6.7.8"
|
46
52
|
|
47
53
|
assert_equal 200, last_response.status
|
48
|
-
assert_equal
|
54
|
+
assert_equal 1, notifications.size
|
55
|
+
notification = notifications.pop
|
56
|
+
assert_equal :safelist, notification[:request].env["rack.attack.match_type"]
|
49
57
|
end
|
50
58
|
end
|