rack-attack 6.1.0 → 6.2.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 +11 -2
- data/lib/rack/attack.rb +136 -124
- data/lib/rack/attack/cache.rb +4 -1
- data/lib/rack/attack/check.rb +2 -1
- data/lib/rack/attack/path_normalizer.rb +20 -18
- data/lib/rack/attack/railtie.rb +21 -0
- data/lib/rack/attack/store_proxy/active_support_redis_store_proxy.rb +3 -1
- data/lib/rack/attack/store_proxy/mem_cache_store_proxy.rb +3 -1
- data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +22 -6
- data/lib/rack/attack/throttle.rb +3 -2
- data/lib/rack/attack/track.rb +6 -5
- data/lib/rack/attack/version.rb +1 -1
- data/spec/acceptance/rails_middleware_spec.rb +41 -0
- data/spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb +1 -3
- data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +7 -1
- data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +6 -1
- data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +3 -3
- data/spec/allow2ban_spec.rb +17 -14
- data/spec/fail2ban_spec.rb +17 -16
- data/spec/rack_attack_instrumentation_spec.rb +1 -1
- data/spec/rack_attack_path_normalizer_spec.rb +2 -2
- data/spec/rack_attack_spec.rb +36 -13
- data/spec/rack_attack_throttle_spec.rb +12 -12
- data/spec/rack_attack_track_spec.rb +8 -5
- data/spec/spec_helper.rb +5 -4
- metadata +30 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ac5af22059fcc24c45b9732a806b13ef8a39b3ab425e713d22b7d0b1c9fbae11
|
4
|
+
data.tar.gz: fdd20e74080d4254d7910be3d1f0343580a2cedd79b18f2448fa753acd9259e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f2a7bd75ab8423dde30e482085b19cb5cfbf7347aed13c94da63d31784939075278cc0f891af450bd33e5ef3de4ea092441b26f2519e28ecb5cbe5c6a16d007
|
7
|
+
data.tar.gz: fbe8d0cc86c52be9a028a4fcb2f8f2399af143b6ccd77c7377cbe5f762bb344bdb80833c5e53221ffefad57d04ee132650b38cd5e9d44c5073e475ba894a1a3f
|
data/README.md
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
__Note__: You are viewing the development version README.
|
2
|
+
For the README consistent with the latest released version see https://github.com/kickstarter/rack-attack/blob/6-stable/README.md.
|
3
|
+
|
1
4
|
# Rack::Attack
|
2
5
|
|
3
6
|
*Rack middleware for blocking & throttling abusive requests*
|
@@ -9,6 +12,7 @@ See the [Backing & Hacking blog post](https://www.kickstarter.com/backing-and-ha
|
|
9
12
|
[](https://badge.fury.io/rb/rack-attack)
|
10
13
|
[](https://travis-ci.org/kickstarter/rack-attack)
|
11
14
|
[](https://codeclimate.com/github/kickstarter/rack-attack)
|
15
|
+
[](https://gitter.im/rack-attack/rack-attack)
|
12
16
|
|
13
17
|
## Table of contents
|
14
18
|
|
@@ -67,14 +71,19 @@ Or install it yourself as:
|
|
67
71
|
|
68
72
|
Then tell your ruby web application to use rack-attack as a middleware.
|
69
73
|
|
70
|
-
a) For __rails__ applications:
|
71
|
-
|
74
|
+
a) For __rails__ applications with versions >= 5.1 it is used by default. For older rails versions you should enable it explicitly:
|
72
75
|
```ruby
|
73
76
|
# In config/application.rb
|
74
77
|
|
75
78
|
config.middleware.use Rack::Attack
|
76
79
|
```
|
77
80
|
|
81
|
+
You can disable it permanently (like for specific environment) or temporarily (can be useful for specific test cases) by writing:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
Rack::Attack.enabled = false
|
85
|
+
```
|
86
|
+
|
78
87
|
b) For __rack__ applications:
|
79
88
|
|
80
89
|
```ruby
|
data/lib/rack/attack.rb
CHANGED
@@ -6,159 +6,171 @@ require 'rack/attack/path_normalizer'
|
|
6
6
|
require 'rack/attack/request'
|
7
7
|
require "ipaddr"
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
9
|
+
require 'rack/attack/railtie' if defined?(::Rails)
|
10
|
+
|
11
|
+
module Rack
|
12
|
+
class Attack
|
13
|
+
class Error < StandardError; end
|
14
|
+
class MisconfiguredStoreError < Error; end
|
15
|
+
class MissingStoreError < Error; end
|
16
|
+
|
17
|
+
autoload :Cache, 'rack/attack/cache'
|
18
|
+
autoload :Check, 'rack/attack/check'
|
19
|
+
autoload :Throttle, 'rack/attack/throttle'
|
20
|
+
autoload :Safelist, 'rack/attack/safelist'
|
21
|
+
autoload :Blocklist, 'rack/attack/blocklist'
|
22
|
+
autoload :Track, 'rack/attack/track'
|
23
|
+
autoload :StoreProxy, 'rack/attack/store_proxy'
|
24
|
+
autoload :DalliProxy, 'rack/attack/store_proxy/dalli_proxy'
|
25
|
+
autoload :MemCacheStoreProxy, 'rack/attack/store_proxy/mem_cache_store_proxy'
|
26
|
+
autoload :RedisProxy, 'rack/attack/store_proxy/redis_proxy'
|
27
|
+
autoload :RedisStoreProxy, 'rack/attack/store_proxy/redis_store_proxy'
|
28
|
+
autoload :RedisCacheStoreProxy, 'rack/attack/store_proxy/redis_cache_store_proxy'
|
29
|
+
autoload :ActiveSupportRedisStoreProxy, 'rack/attack/store_proxy/active_support_redis_store_proxy'
|
30
|
+
autoload :Fail2Ban, 'rack/attack/fail2ban'
|
31
|
+
autoload :Allow2Ban, 'rack/attack/allow2ban'
|
32
|
+
|
33
|
+
class << self
|
34
|
+
attr_accessor :enabled, :notifier, :blocklisted_response, :throttled_response,
|
35
|
+
:anonymous_blocklists, :anonymous_safelists
|
36
|
+
|
37
|
+
def safelist(name = nil, &block)
|
38
|
+
safelist = Safelist.new(name, &block)
|
39
|
+
|
40
|
+
if name
|
41
|
+
safelists[name] = safelist
|
42
|
+
else
|
43
|
+
anonymous_safelists << safelist
|
44
|
+
end
|
39
45
|
end
|
40
|
-
end
|
41
46
|
|
42
|
-
|
43
|
-
|
47
|
+
def blocklist(name = nil, &block)
|
48
|
+
blocklist = Blocklist.new(name, &block)
|
44
49
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
50
|
+
if name
|
51
|
+
blocklists[name] = blocklist
|
52
|
+
else
|
53
|
+
anonymous_blocklists << blocklist
|
54
|
+
end
|
49
55
|
end
|
50
|
-
end
|
51
56
|
|
52
|
-
|
53
|
-
|
54
|
-
|
57
|
+
def blocklist_ip(ip_address)
|
58
|
+
anonymous_blocklists << Blocklist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) }
|
59
|
+
end
|
55
60
|
|
56
|
-
|
57
|
-
|
58
|
-
|
61
|
+
def safelist_ip(ip_address)
|
62
|
+
anonymous_safelists << Safelist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) }
|
63
|
+
end
|
59
64
|
|
60
|
-
|
61
|
-
|
62
|
-
|
65
|
+
def throttle(name, options, &block)
|
66
|
+
throttles[name] = Throttle.new(name, options, &block)
|
67
|
+
end
|
63
68
|
|
64
|
-
|
65
|
-
|
66
|
-
|
69
|
+
def track(name, options = {}, &block)
|
70
|
+
tracks[name] = Track.new(name, options, &block)
|
71
|
+
end
|
67
72
|
|
68
|
-
|
69
|
-
|
70
|
-
|
73
|
+
def safelists
|
74
|
+
@safelists ||= {}
|
75
|
+
end
|
71
76
|
|
72
|
-
|
73
|
-
|
74
|
-
|
77
|
+
def blocklists
|
78
|
+
@blocklists ||= {}
|
79
|
+
end
|
75
80
|
|
76
|
-
|
77
|
-
|
78
|
-
|
81
|
+
def throttles
|
82
|
+
@throttles ||= {}
|
83
|
+
end
|
79
84
|
|
80
|
-
|
81
|
-
|
82
|
-
|
85
|
+
def tracks
|
86
|
+
@tracks ||= {}
|
87
|
+
end
|
83
88
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
89
|
+
def safelisted?(request)
|
90
|
+
anonymous_safelists.any? { |safelist| safelist.matched_by?(request) } ||
|
91
|
+
safelists.any? { |_name, safelist| safelist.matched_by?(request) }
|
92
|
+
end
|
88
93
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
94
|
+
def blocklisted?(request)
|
95
|
+
anonymous_blocklists.any? { |blocklist| blocklist.matched_by?(request) } ||
|
96
|
+
blocklists.any? { |_name, blocklist| blocklist.matched_by?(request) }
|
97
|
+
end
|
93
98
|
|
94
|
-
|
95
|
-
|
96
|
-
|
99
|
+
def throttled?(request)
|
100
|
+
throttles.any? do |_name, throttle|
|
101
|
+
throttle.matched_by?(request)
|
102
|
+
end
|
97
103
|
end
|
98
|
-
end
|
99
104
|
|
100
|
-
|
101
|
-
|
102
|
-
|
105
|
+
def tracked?(request)
|
106
|
+
tracks.each_value do |track|
|
107
|
+
track.matched_by?(request)
|
108
|
+
end
|
103
109
|
end
|
104
|
-
end
|
105
110
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
111
|
+
def instrument(request)
|
112
|
+
if notifier
|
113
|
+
event_type = request.env["rack.attack.match_type"]
|
114
|
+
notifier.instrument("#{event_type}.rack_attack", request: request)
|
110
115
|
|
111
|
-
|
112
|
-
|
116
|
+
# Deprecated: Keeping just for backwards compatibility
|
117
|
+
notifier.instrument("rack.attack", request: request)
|
118
|
+
end
|
113
119
|
end
|
114
|
-
end
|
115
120
|
|
116
|
-
|
117
|
-
|
121
|
+
def cache
|
122
|
+
@cache ||= Cache.new
|
123
|
+
end
|
124
|
+
|
125
|
+
def clear_configuration
|
126
|
+
@safelists = {}
|
127
|
+
@blocklists = {}
|
128
|
+
@throttles = {}
|
129
|
+
@tracks = {}
|
130
|
+
self.anonymous_blocklists = []
|
131
|
+
self.anonymous_safelists = []
|
132
|
+
end
|
133
|
+
|
134
|
+
def clear!
|
135
|
+
warn "[DEPRECATION] Rack::Attack.clear! is deprecated. Please use Rack::Attack.clear_configuration instead"
|
136
|
+
clear_configuration
|
137
|
+
end
|
118
138
|
end
|
119
139
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
140
|
+
# Set defaults
|
141
|
+
@enabled = true
|
142
|
+
@anonymous_blocklists = []
|
143
|
+
@anonymous_safelists = []
|
144
|
+
@notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
|
145
|
+
@blocklisted_response = lambda { |_env| [403, { 'Content-Type' => 'text/plain' }, ["Forbidden\n"]] }
|
146
|
+
@throttled_response = lambda do |env|
|
147
|
+
retry_after = (env['rack.attack.match_data'] || {})[:period]
|
148
|
+
[429, { 'Content-Type' => 'text/plain', 'Retry-After' => retry_after.to_s }, ["Retry later\n"]]
|
124
149
|
end
|
125
150
|
|
126
|
-
def
|
127
|
-
|
128
|
-
clear_configuration
|
151
|
+
def initialize(app)
|
152
|
+
@app = app
|
129
153
|
end
|
130
|
-
end
|
131
154
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
@throttled_response = lambda { |env|
|
138
|
-
retry_after = (env['rack.attack.match_data'] || {})[:period]
|
139
|
-
[429, { 'Content-Type' => 'text/plain', 'Retry-After' => retry_after.to_s }, ["Retry later\n"]]
|
140
|
-
}
|
141
|
-
|
142
|
-
def initialize(app)
|
143
|
-
@app = app
|
144
|
-
end
|
155
|
+
def call(env)
|
156
|
+
return @app.call(env) unless self.class.enabled
|
157
|
+
|
158
|
+
env['PATH_INFO'] = PathNormalizer.normalize_path(env['PATH_INFO'])
|
159
|
+
request = Rack::Attack::Request.new(env)
|
145
160
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
else
|
157
|
-
tracked?(request)
|
158
|
-
@app.call(env)
|
161
|
+
if safelisted?(request)
|
162
|
+
@app.call(env)
|
163
|
+
elsif blocklisted?(request)
|
164
|
+
self.class.blocklisted_response.call(env)
|
165
|
+
elsif throttled?(request)
|
166
|
+
self.class.throttled_response.call(env)
|
167
|
+
else
|
168
|
+
tracked?(request)
|
169
|
+
@app.call(env)
|
170
|
+
end
|
159
171
|
end
|
160
|
-
end
|
161
172
|
|
162
|
-
|
163
|
-
|
173
|
+
extend Forwardable
|
174
|
+
def_delegators self, :safelisted?, :blocklisted?, :throttled?, :tracked?
|
175
|
+
end
|
164
176
|
end
|
data/lib/rack/attack/cache.rb
CHANGED
@@ -73,7 +73,10 @@ module Rack
|
|
73
73
|
|
74
74
|
def enforce_store_method_presence!(method_name)
|
75
75
|
if !store.respond_to?(method_name)
|
76
|
-
raise
|
76
|
+
raise(
|
77
|
+
Rack::Attack::MisconfiguredStoreError,
|
78
|
+
"Configured store #{store.class.name} doesn't respond to ##{method_name} method"
|
79
|
+
)
|
77
80
|
end
|
78
81
|
end
|
79
82
|
end
|
data/lib/rack/attack/check.rb
CHANGED
@@ -1,24 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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).
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
module FallbackPathNormalizer
|
14
|
+
def self.normalize_path(path)
|
15
|
+
path
|
16
|
+
end
|
15
17
|
end
|
16
|
-
end
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
24
26
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class Attack
|
5
|
+
class Railtie < ::Rails::Railtie
|
6
|
+
initializer 'rack.attack.middleware', after: :load_config_initializers, before: :build_middleware_stack do |app|
|
7
|
+
if Gem::Version.new(::Rails::VERSION::STRING) >= Gem::Version.new("5.1")
|
8
|
+
middlewares = app.config.middleware
|
9
|
+
operations = middlewares.send(:operations) + middlewares.send(:delete_operations)
|
10
|
+
|
11
|
+
use_middleware = operations.none? do |operation|
|
12
|
+
middleware = operation[1]
|
13
|
+
middleware.include?(Rack::Attack)
|
14
|
+
end
|
15
|
+
|
16
|
+
middlewares.use(Rack::Attack) if use_middleware
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -7,7 +7,9 @@ module Rack
|
|
7
7
|
module StoreProxy
|
8
8
|
class ActiveSupportRedisStoreProxy < SimpleDelegator
|
9
9
|
def self.handle?(store)
|
10
|
-
defined?(::Redis) &&
|
10
|
+
defined?(::Redis) &&
|
11
|
+
defined?(::ActiveSupport::Cache::RedisStore) &&
|
12
|
+
store.is_a?(::ActiveSupport::Cache::RedisStore)
|
11
13
|
end
|
12
14
|
|
13
15
|
def increment(name, amount = 1, options = {})
|
@@ -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 = {})
|