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.
- checksums.yaml +4 -4
- data/README.md +78 -27
- data/Rakefile +3 -1
- data/lib/rack/attack.rb +138 -149
- data/lib/rack/attack/allow2ban.rb +2 -0
- data/lib/rack/attack/blocklist.rb +3 -1
- data/lib/rack/attack/cache.rb +9 -4
- data/lib/rack/attack/check.rb +5 -2
- data/lib/rack/attack/fail2ban.rb +2 -0
- data/lib/rack/attack/path_normalizer.rb +22 -18
- data/lib/rack/attack/railtie.rb +13 -0
- data/lib/rack/attack/request.rb +2 -0
- data/lib/rack/attack/safelist.rb +3 -1
- data/lib/rack/attack/store_proxy.rb +12 -14
- data/lib/rack/attack/store_proxy/active_support_redis_store_proxy.rb +39 -0
- data/lib/rack/attack/store_proxy/dalli_proxy.rb +27 -13
- data/lib/rack/attack/store_proxy/mem_cache_store_proxy.rb +3 -1
- data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +23 -9
- data/lib/rack/attack/store_proxy/redis_proxy.rb +16 -10
- data/lib/rack/attack/store_proxy/redis_store_proxy.rb +5 -5
- data/lib/rack/attack/throttle.rb +12 -8
- data/lib/rack/attack/track.rb +9 -6
- data/lib/rack/attack/version.rb +3 -1
- data/spec/acceptance/allow2ban_spec.rb +2 -0
- data/spec/acceptance/blocking_ip_spec.rb +4 -2
- data/spec/acceptance/blocking_spec.rb +45 -3
- data/spec/acceptance/blocking_subnet_spec.rb +4 -2
- data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +50 -39
- data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +38 -29
- data/spec/acceptance/cache_store_config_for_throttle_spec.rb +2 -0
- data/spec/acceptance/cache_store_config_with_rails_spec.rb +2 -0
- data/spec/acceptance/customizing_blocked_response_spec.rb +2 -0
- data/spec/acceptance/customizing_throttled_response_spec.rb +2 -0
- data/spec/acceptance/extending_request_object_spec.rb +2 -0
- data/spec/acceptance/fail2ban_spec.rb +2 -0
- data/spec/acceptance/rails_middleware_spec.rb +35 -0
- data/spec/acceptance/safelisting_ip_spec.rb +4 -2
- data/spec/acceptance/safelisting_spec.rb +57 -3
- data/spec/acceptance/safelisting_subnet_spec.rb +4 -2
- data/spec/acceptance/stores/active_support_dalli_store_spec.rb +2 -0
- data/spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb +1 -3
- data/spec/acceptance/stores/active_support_mem_cache_store_spec.rb +2 -0
- data/spec/acceptance/stores/active_support_memory_store_spec.rb +2 -0
- data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +9 -1
- data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +8 -1
- data/spec/acceptance/stores/active_support_redis_store_spec.rb +3 -1
- data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +5 -3
- data/spec/acceptance/stores/dalli_client_spec.rb +2 -0
- data/spec/acceptance/stores/redis_store_spec.rb +2 -0
- data/spec/acceptance/throttling_spec.rb +7 -5
- data/spec/acceptance/track_spec.rb +5 -3
- data/spec/acceptance/track_throttle_spec.rb +5 -3
- data/spec/allow2ban_spec.rb +20 -15
- data/spec/fail2ban_spec.rb +20 -17
- data/spec/integration/offline_spec.rb +3 -1
- data/spec/rack_attack_dalli_proxy_spec.rb +2 -0
- data/spec/rack_attack_instrumentation_spec.rb +42 -0
- data/spec/rack_attack_path_normalizer_spec.rb +4 -2
- data/spec/rack_attack_request_spec.rb +2 -0
- data/spec/rack_attack_spec.rb +38 -34
- data/spec/rack_attack_throttle_spec.rb +50 -19
- data/spec/rack_attack_track_spec.rb +12 -7
- data/spec/spec_helper.rb +12 -8
- data/spec/support/cache_store_helper.rb +2 -0
- metadata +44 -28
- data/lib/rack/attack/store_proxy/mem_cache_proxy.rb +0 -50
data/lib/rack/attack/cache.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rack
|
2
4
|
class Attack
|
3
5
|
class Cache
|
@@ -27,7 +29,7 @@ module Rack
|
|
27
29
|
end
|
28
30
|
|
29
31
|
def write(unprefixed_key, value, expires_in)
|
30
|
-
store.write("#{prefix}:#{unprefixed_key}", value, :
|
32
|
+
store.write("#{prefix}:#{unprefixed_key}", value, expires_in: expires_in)
|
31
33
|
end
|
32
34
|
|
33
35
|
def reset_count(unprefixed_key, period)
|
@@ -52,13 +54,13 @@ module Rack
|
|
52
54
|
enforce_store_presence!
|
53
55
|
enforce_store_method_presence!(:increment)
|
54
56
|
|
55
|
-
result = store.increment(key, 1, :
|
57
|
+
result = store.increment(key, 1, expires_in: expires_in)
|
56
58
|
|
57
59
|
# NB: Some stores return nil when incrementing uninitialized values
|
58
60
|
if result.nil?
|
59
61
|
enforce_store_method_presence!(:write)
|
60
62
|
|
61
|
-
store.write(key, 1, :
|
63
|
+
store.write(key, 1, expires_in: expires_in)
|
62
64
|
end
|
63
65
|
result || 1
|
64
66
|
end
|
@@ -71,7 +73,10 @@ module Rack
|
|
71
73
|
|
72
74
|
def enforce_store_method_presence!(method_name)
|
73
75
|
if !store.respond_to?(method_name)
|
74
|
-
raise
|
76
|
+
raise(
|
77
|
+
Rack::Attack::MisconfiguredStoreError,
|
78
|
+
"Configured store #{store.class.name} doesn't respond to ##{method_name} method"
|
79
|
+
)
|
75
80
|
end
|
76
81
|
end
|
77
82
|
end
|
data/lib/rack/attack/check.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rack
|
2
4
|
class Attack
|
3
5
|
class Check
|
4
6
|
attr_reader :name, :block, :type
|
5
|
-
def initialize(name, options = {}, block)
|
6
|
-
@name
|
7
|
+
def initialize(name, options = {}, &block)
|
8
|
+
@name = name
|
9
|
+
@block = block
|
7
10
|
@type = options.fetch(:type, nil)
|
8
11
|
end
|
9
12
|
|
data/lib/rack/attack/fail2ban.rb
CHANGED
@@ -1,22 +1,26 @@
|
|
1
|
-
|
2
|
-
# When using Rack::Attack with a Rails app, developers expect the request path
|
3
|
-
# to be normalized. In particular, trailing slashes are stripped.
|
4
|
-
# (See https://git.io/v0rrR for implementation.)
|
5
|
-
#
|
6
|
-
# Look for an ActionDispatch utility class that Rails folks would expect
|
7
|
-
# to normalize request paths. If unavailable, use a fallback class that
|
8
|
-
# doesn't normalize the path (as a non-Rails rack app developer expects).
|
1
|
+
# frozen_string_literal: true
|
9
2
|
|
10
|
-
|
11
|
-
|
12
|
-
|
3
|
+
module Rack
|
4
|
+
class Attack
|
5
|
+
# When using Rack::Attack with a Rails app, developers expect the request path
|
6
|
+
# to be normalized. In particular, trailing slashes are stripped.
|
7
|
+
# (See https://git.io/v0rrR for implementation.)
|
8
|
+
#
|
9
|
+
# Look for an ActionDispatch utility class that Rails folks would expect
|
10
|
+
# to normalize request paths. If unavailable, use a fallback class that
|
11
|
+
# doesn't normalize the path (as a non-Rails rack app developer expects).
|
12
|
+
|
13
|
+
module FallbackPathNormalizer
|
14
|
+
def self.normalize_path(path)
|
15
|
+
path
|
16
|
+
end
|
13
17
|
end
|
14
|
-
end
|
15
18
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
PathNormalizer = if defined?(::ActionDispatch::Journey::Router::Utils)
|
20
|
+
# For Rails apps
|
21
|
+
::ActionDispatch::Journey::Router::Utils
|
22
|
+
else
|
23
|
+
FallbackPathNormalizer
|
24
|
+
end
|
25
|
+
end
|
22
26
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class Attack
|
5
|
+
class Railtie < ::Rails::Railtie
|
6
|
+
initializer "rack-attack.middleware" do |app|
|
7
|
+
if Gem::Version.new(::Rails::VERSION::STRING) >= Gem::Version.new("5.1")
|
8
|
+
app.middleware.use(Rack::Attack)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/rack/attack/request.rb
CHANGED
data/lib/rack/attack/safelist.rb
CHANGED
@@ -1,22 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rack
|
2
4
|
class Attack
|
3
5
|
module StoreProxy
|
4
|
-
PROXIES = [
|
6
|
+
PROXIES = [
|
7
|
+
DalliProxy,
|
8
|
+
MemCacheStoreProxy,
|
9
|
+
RedisStoreProxy,
|
10
|
+
RedisProxy,
|
11
|
+
RedisCacheStoreProxy,
|
12
|
+
ActiveSupportRedisStoreProxy
|
13
|
+
].freeze
|
5
14
|
|
6
15
|
def self.build(store)
|
7
|
-
|
8
|
-
klass
|
9
|
-
klass ? klass.new(client) : client
|
10
|
-
end
|
11
|
-
|
12
|
-
def self.unwrap_active_support_stores(store)
|
13
|
-
# ActiveSupport::Cache::RedisStore doesn't expose any way to set an expiry,
|
14
|
-
# so use the raw Redis::Store instead.
|
15
|
-
if store.class.name == 'ActiveSupport::Cache::RedisStore'
|
16
|
-
store.instance_variable_get(:@data)
|
17
|
-
else
|
18
|
-
store
|
19
|
-
end
|
16
|
+
klass = PROXIES.find { |proxy| proxy.handle?(store) }
|
17
|
+
klass ? klass.new(store) : store
|
20
18
|
end
|
21
19
|
end
|
22
20
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'delegate'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
class Attack
|
7
|
+
module StoreProxy
|
8
|
+
class ActiveSupportRedisStoreProxy < SimpleDelegator
|
9
|
+
def self.handle?(store)
|
10
|
+
defined?(::Redis) &&
|
11
|
+
defined?(::ActiveSupport::Cache::RedisStore) &&
|
12
|
+
store.is_a?(::ActiveSupport::Cache::RedisStore)
|
13
|
+
end
|
14
|
+
|
15
|
+
def increment(name, amount = 1, options = {})
|
16
|
+
# #increment ignores options[:expires_in].
|
17
|
+
#
|
18
|
+
# So in order to workaround this we use #write (which sets expiration) to initialize
|
19
|
+
# the counter. After that we continue using the original #increment.
|
20
|
+
if options[:expires_in] && !read(name)
|
21
|
+
write(name, amount, options)
|
22
|
+
|
23
|
+
amount
|
24
|
+
else
|
25
|
+
super
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def read(name, options = {})
|
30
|
+
super(name, options.merge!(raw: true))
|
31
|
+
end
|
32
|
+
|
33
|
+
def write(name, value, options = {})
|
34
|
+
super(name, value, options.merge!(raw: true))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'delegate'
|
2
4
|
|
3
5
|
module Rack
|
@@ -22,31 +24,35 @@ module Rack
|
|
22
24
|
end
|
23
25
|
|
24
26
|
def read(key)
|
25
|
-
|
26
|
-
client
|
27
|
+
rescuing do
|
28
|
+
with do |client|
|
29
|
+
client.get(key)
|
30
|
+
end
|
27
31
|
end
|
28
|
-
rescue Dalli::DalliError
|
29
32
|
end
|
30
33
|
|
31
34
|
def write(key, value, options = {})
|
32
|
-
|
33
|
-
|
35
|
+
rescuing do
|
36
|
+
with do |client|
|
37
|
+
client.set(key, value, options.fetch(:expires_in, 0), raw: true)
|
38
|
+
end
|
34
39
|
end
|
35
|
-
rescue Dalli::DalliError
|
36
40
|
end
|
37
41
|
|
38
42
|
def increment(key, amount, options = {})
|
39
|
-
|
40
|
-
|
43
|
+
rescuing do
|
44
|
+
with do |client|
|
45
|
+
client.incr(key, amount, options.fetch(:expires_in, 0), amount)
|
46
|
+
end
|
41
47
|
end
|
42
|
-
rescue Dalli::DalliError
|
43
48
|
end
|
44
49
|
|
45
50
|
def delete(key)
|
46
|
-
|
47
|
-
client
|
51
|
+
rescuing do
|
52
|
+
with do |client|
|
53
|
+
client.delete(key)
|
54
|
+
end
|
48
55
|
end
|
49
|
-
rescue Dalli::DalliError
|
50
56
|
end
|
51
57
|
|
52
58
|
private
|
@@ -54,10 +60,18 @@ module Rack
|
|
54
60
|
def stub_with_if_missing
|
55
61
|
unless __getobj__.respond_to?(:with)
|
56
62
|
class << self
|
57
|
-
def with
|
63
|
+
def with
|
64
|
+
yield __getobj__
|
65
|
+
end
|
58
66
|
end
|
59
67
|
end
|
60
68
|
end
|
69
|
+
|
70
|
+
def rescuing
|
71
|
+
yield
|
72
|
+
rescue Dalli::DalliError
|
73
|
+
nil
|
74
|
+
end
|
61
75
|
end
|
62
76
|
end
|
63
77
|
end
|
@@ -7,7 +7,9 @@ module Rack
|
|
7
7
|
module StoreProxy
|
8
8
|
class MemCacheStoreProxy < SimpleDelegator
|
9
9
|
def self.handle?(store)
|
10
|
-
defined?(::Dalli) &&
|
10
|
+
defined?(::Dalli) &&
|
11
|
+
defined?(::ActiveSupport::Cache::MemCacheStore) &&
|
12
|
+
store.is_a?(::ActiveSupport::Cache::MemCacheStore)
|
11
13
|
end
|
12
14
|
|
13
15
|
def write(name, value, options = {})
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'delegate'
|
2
4
|
|
3
5
|
module Rack
|
@@ -5,7 +7,7 @@ module Rack
|
|
5
7
|
module StoreProxy
|
6
8
|
class RedisCacheStoreProxy < SimpleDelegator
|
7
9
|
def self.handle?(store)
|
8
|
-
|
10
|
+
store.class.name == "ActiveSupport::Cache::RedisCacheStore"
|
9
11
|
end
|
10
12
|
|
11
13
|
def increment(name, amount = 1, options = {})
|
@@ -13,21 +15,33 @@ module Rack
|
|
13
15
|
#
|
14
16
|
# So in order to workaround this we use RedisCacheStore#write (which sets expiration) to initialize
|
15
17
|
# the counter. After that we continue using the original RedisCacheStore#increment.
|
16
|
-
|
17
|
-
|
18
|
+
rescuing do
|
19
|
+
if options[:expires_in] && !read(name)
|
20
|
+
write(name, amount, options)
|
18
21
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
+
amount
|
23
|
+
else
|
24
|
+
super
|
25
|
+
end
|
22
26
|
end
|
23
27
|
end
|
24
28
|
|
25
|
-
def read(
|
26
|
-
super
|
29
|
+
def read(*_args)
|
30
|
+
rescuing { super }
|
27
31
|
end
|
28
32
|
|
29
33
|
def write(name, value, options = {})
|
30
|
-
|
34
|
+
rescuing do
|
35
|
+
super(name, value, options.merge!(raw: true))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def rescuing
|
42
|
+
yield
|
43
|
+
rescue Redis::BaseError
|
44
|
+
nil
|
31
45
|
end
|
32
46
|
end
|
33
47
|
end
|
@@ -19,34 +19,40 @@ module Rack
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def read(key)
|
22
|
-
get(key)
|
23
|
-
rescue Redis::BaseError
|
22
|
+
rescuing { get(key) }
|
24
23
|
end
|
25
24
|
|
26
25
|
def write(key, value, options = {})
|
27
26
|
if (expires_in = options[:expires_in])
|
28
|
-
setex(key, expires_in, value)
|
27
|
+
rescuing { setex(key, expires_in, value) }
|
29
28
|
else
|
30
|
-
set(key, value)
|
29
|
+
rescuing { set(key, value) }
|
31
30
|
end
|
32
|
-
rescue Redis::BaseError
|
33
31
|
end
|
34
32
|
|
35
33
|
def increment(key, amount, options = {})
|
36
34
|
count = nil
|
37
35
|
|
38
|
-
|
39
|
-
|
40
|
-
|
36
|
+
rescuing do
|
37
|
+
pipelined do
|
38
|
+
count = incrby(key, amount)
|
39
|
+
expire(key, options[:expires_in]) if options[:expires_in]
|
40
|
+
end
|
41
41
|
end
|
42
42
|
|
43
43
|
count.value if count
|
44
|
-
rescue Redis::BaseError
|
45
44
|
end
|
46
45
|
|
47
46
|
def delete(key, _options = {})
|
48
|
-
del(key)
|
47
|
+
rescuing { del(key) }
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def rescuing
|
53
|
+
yield
|
49
54
|
rescue Redis::BaseError
|
55
|
+
nil
|
50
56
|
end
|
51
57
|
end
|
52
58
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'delegate'
|
2
4
|
|
3
5
|
module Rack
|
@@ -9,17 +11,15 @@ module Rack
|
|
9
11
|
end
|
10
12
|
|
11
13
|
def read(key)
|
12
|
-
get(key, raw: true)
|
13
|
-
rescue Redis::BaseError
|
14
|
+
rescuing { get(key, raw: true) }
|
14
15
|
end
|
15
16
|
|
16
17
|
def write(key, value, options = {})
|
17
18
|
if (expires_in = options[:expires_in])
|
18
|
-
setex(key, expires_in, value, raw: true)
|
19
|
+
rescuing { setex(key, expires_in, value, raw: true) }
|
19
20
|
else
|
20
|
-
set(key, value, raw: true)
|
21
|
+
rescuing { set(key, value, raw: true) }
|
21
22
|
end
|
22
|
-
rescue Redis::BaseError
|
23
23
|
end
|
24
24
|
end
|
25
25
|
end
|