rack-attack 5.2.0 → 5.3.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 +13 -24
- data/Rakefile +1 -1
- data/lib/rack/attack.rb +28 -23
- data/lib/rack/attack/allow2ban.rb +1 -0
- data/lib/rack/attack/blocklist.rb +0 -1
- data/lib/rack/attack/cache.rb +1 -2
- data/lib/rack/attack/check.rb +1 -2
- data/lib/rack/attack/fail2ban.rb +2 -1
- data/lib/rack/attack/path_normalizer.rb +6 -8
- data/lib/rack/attack/safelist.rb +0 -1
- data/lib/rack/attack/store_proxy.rb +2 -4
- data/lib/rack/attack/store_proxy/dalli_proxy.rb +2 -3
- data/lib/rack/attack/store_proxy/mem_cache_proxy.rb +4 -5
- data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +30 -0
- data/lib/rack/attack/store_proxy/redis_store_proxy.rb +4 -11
- data/lib/rack/attack/version.rb +1 -1
- data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +2 -2
- data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +2 -2
- data/spec/acceptance/cache_store_config_for_throttle_spec.rb +1 -1
- data/spec/acceptance/customizing_blocked_response_spec.rb +1 -1
- data/spec/acceptance/customizing_throttled_response_spec.rb +1 -1
- data/spec/acceptance/safelisting_ip_spec.rb +0 -1
- data/spec/acceptance/stores/mem_cache_store_spec.rb +38 -0
- data/spec/acceptance/stores/redis_cache_store_spec.rb +41 -0
- data/spec/allow2ban_spec.rb +6 -6
- data/spec/fail2ban_spec.rb +7 -7
- data/spec/integration/rack_attack_cache_spec.rb +4 -1
- data/spec/rack_attack_dalli_proxy_spec.rb +0 -2
- data/spec/rack_attack_spec.rb +6 -6
- data/spec/rack_attack_throttle_spec.rb +7 -7
- data/spec/rack_attack_track_spec.rb +5 -5
- data/spec/spec_helper.rb +3 -4
- data/spec/support/cache_store_helper.rb +58 -0
- metadata +65 -44
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c8f4c760b1ff68e6fbe79cf7576cbe2e0cc79ef5e2734ff55e63b46665ed6731
|
4
|
+
data.tar.gz: 75d7dd7fc98035d3961952582373af467fcfd225dac11af8db4aaf1cbaaf44b8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 53ecb94578ea2497b660ca508b572b8a1f9a5a40e986b4bc6f834e05f90ea49eb4e721aefe4bd431b26754aeb33440fa4b43cc204705c035e61ada8e79d543dc
|
7
|
+
data.tar.gz: 22dbd120cdc634115f7096d5e9e4a44cc4ec97ae5dd11fc746b0e21e6b2ad860bb8bbd62c0bf7b7ee0f63d5803c568579b5569d3f849159751b25c236ede1520
|
data/README.md
CHANGED
@@ -4,9 +4,9 @@
|
|
4
4
|
|
5
5
|
Protect your Rails and Rack apps from bad clients. Rack::Attack lets you easily decide when to *allow*, *block* and *throttle* based on properties of the request.
|
6
6
|
|
7
|
-
See the [Backing & Hacking blog post](
|
7
|
+
See the [Backing & Hacking blog post](https://www.kickstarter.com/backing-and-hacking/rack-attack-protection-from-abusive-clients) introducing Rack::Attack.
|
8
8
|
|
9
|
-
[](
|
9
|
+
[](https://badge.fury.io/rb/rack-attack)
|
10
10
|
[](https://travis-ci.org/kickstarter/rack-attack)
|
11
11
|
[](https://codeclimate.com/github/kickstarter/rack-attack)
|
12
12
|
|
@@ -133,7 +133,7 @@ Rack::Attack.blocklist_ip("1.2.0.0/16")
|
|
133
133
|
|
134
134
|
#### `blocklist(name, &block)`
|
135
135
|
|
136
|
-
Name your custom blocklist and make your ruby-block argument
|
136
|
+
Name your custom blocklist and make your ruby-block argument return a truthy value if you want the request to be blocked, and falsy otherwise.
|
137
137
|
|
138
138
|
The request object is a [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request).
|
139
139
|
|
@@ -155,8 +155,8 @@ end
|
|
155
155
|
#### Fail2Ban
|
156
156
|
|
157
157
|
`Fail2Ban.filter` can be used within a blocklist to block all requests from misbehaving clients.
|
158
|
-
This pattern is inspired by [fail2ban](
|
159
|
-
See the [fail2ban documentation](
|
158
|
+
This pattern is inspired by [fail2ban](https://www.fail2ban.org/wiki/index.php/Main_Page).
|
159
|
+
See the [fail2ban documentation](https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jail_Options) for more details on
|
160
160
|
how the parameters work. For multiple filters, be sure to put each filter in a separate blocklist and use a unique discriminator for each fail2ban filter.
|
161
161
|
|
162
162
|
Fail2ban state is stored in a [configurable cache](#cache-store-configuration) (which defaults to `Rails.cache` if present).
|
@@ -272,7 +272,7 @@ Note that `Rack::Attack.cache` is only used for throttling, allow2ban and fail2b
|
|
272
272
|
|
273
273
|
## Customizing responses
|
274
274
|
|
275
|
-
Customize the response of blocklisted and throttled requests using an object that adheres to the [Rack app interface](http://
|
275
|
+
Customize the response of blocklisted and throttled requests using an object that adheres to the [Rack app interface](http://www.rubydoc.info/github/rack/rack/file/SPEC).
|
276
276
|
|
277
277
|
```ruby
|
278
278
|
Rack::Attack.blocklisted_response = lambda do |env|
|
@@ -388,7 +388,7 @@ so try to keep the number of throttle checks per request low.
|
|
388
388
|
If a request is blocklisted or throttled, the response is a very simple Rack response.
|
389
389
|
A single typical ruby web server thread can block several hundred requests per second.
|
390
390
|
|
391
|
-
Rack::Attack complements tools like `iptables` and nginx's [limit_conn_zone module](
|
391
|
+
Rack::Attack complements tools like `iptables` and nginx's [limit_conn_zone module](https://nginx.org/en/docs/http/ngx_http_limit_conn_module.html#limit_conn_zone).
|
392
392
|
|
393
393
|
## Motivation
|
394
394
|
|
@@ -402,26 +402,15 @@ less on short-term, one-off hacks to block a particular attack.
|
|
402
402
|
|
403
403
|
## Contributing
|
404
404
|
|
405
|
-
|
406
|
-
a safe, welcoming space for collaboration, and contributors are expected to
|
407
|
-
adhere to the [Code of Conduct](CODE_OF_CONDUCT.md).
|
405
|
+
Check out the [Contributing guide](CONTRIBUTING.md).
|
408
406
|
|
409
|
-
|
407
|
+
## Code of Conduct
|
410
408
|
|
411
|
-
|
412
|
-
[Memcached](https://memcached.org/) running locally and bound to IP `127.0.0.1` on
|
413
|
-
default ports (`6379` for Redis, and `11211` for Memcached) and able to be
|
414
|
-
accessed without authentication.
|
409
|
+
This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Code of Conduct](CODE_OF_CONDUCT.md).
|
415
410
|
|
416
|
-
|
417
|
-
```sh
|
418
|
-
bundle install
|
419
|
-
```
|
411
|
+
## Development setup
|
420
412
|
|
421
|
-
|
422
|
-
```sh
|
423
|
-
bundle exec rake
|
424
|
-
```
|
413
|
+
Check out the [Development guide](docs/development.md).
|
425
414
|
|
426
415
|
## Mailing list
|
427
416
|
|
@@ -434,4 +423,4 @@ New releases of Rack::Attack are announced on
|
|
434
423
|
|
435
424
|
Copyright Kickstarter, PBC.
|
436
425
|
|
437
|
-
Released under an [MIT License](
|
426
|
+
Released under an [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
data/lib/rack/attack.rb
CHANGED
@@ -8,21 +8,21 @@ class Rack::Attack
|
|
8
8
|
class MisconfiguredStoreError < StandardError; end
|
9
9
|
class MissingStoreError < StandardError; end
|
10
10
|
|
11
|
-
autoload :Cache,
|
12
|
-
autoload :Check,
|
13
|
-
autoload :Throttle,
|
14
|
-
autoload :Safelist,
|
15
|
-
autoload :Blocklist,
|
16
|
-
autoload :Track,
|
17
|
-
autoload :StoreProxy,
|
18
|
-
autoload :DalliProxy,
|
19
|
-
autoload :MemCacheProxy,
|
20
|
-
autoload :RedisStoreProxy,
|
21
|
-
autoload :
|
22
|
-
autoload :
|
11
|
+
autoload :Cache, 'rack/attack/cache'
|
12
|
+
autoload :Check, 'rack/attack/check'
|
13
|
+
autoload :Throttle, 'rack/attack/throttle'
|
14
|
+
autoload :Safelist, 'rack/attack/safelist'
|
15
|
+
autoload :Blocklist, 'rack/attack/blocklist'
|
16
|
+
autoload :Track, 'rack/attack/track'
|
17
|
+
autoload :StoreProxy, 'rack/attack/store_proxy'
|
18
|
+
autoload :DalliProxy, 'rack/attack/store_proxy/dalli_proxy'
|
19
|
+
autoload :MemCacheProxy, 'rack/attack/store_proxy/mem_cache_proxy'
|
20
|
+
autoload :RedisStoreProxy, 'rack/attack/store_proxy/redis_store_proxy'
|
21
|
+
autoload :RedisCacheStoreProxy, 'rack/attack/store_proxy/redis_cache_store_proxy'
|
22
|
+
autoload :Fail2Ban, 'rack/attack/fail2ban'
|
23
|
+
autoload :Allow2Ban, 'rack/attack/allow2ban'
|
23
24
|
|
24
25
|
class << self
|
25
|
-
|
26
26
|
attr_accessor :notifier, :blocklisted_response, :throttled_response
|
27
27
|
|
28
28
|
def safelist(name, &block)
|
@@ -64,8 +64,11 @@ class Rack::Attack
|
|
64
64
|
end
|
65
65
|
|
66
66
|
def safelists; @safelists ||= {}; end
|
67
|
+
|
67
68
|
def blocklists; @blocklists ||= {}; end
|
69
|
+
|
68
70
|
def throttles; @throttles ||= {}; end
|
71
|
+
|
69
72
|
def tracks; @tracks ||= {}; end
|
70
73
|
|
71
74
|
def whitelists
|
@@ -99,7 +102,7 @@ class Rack::Attack
|
|
99
102
|
end
|
100
103
|
|
101
104
|
def throttled?(req)
|
102
|
-
throttles.any? do |
|
105
|
+
throttles.any? do |_name, throttle|
|
103
106
|
throttle[req]
|
104
107
|
end
|
105
108
|
end
|
@@ -118,15 +121,20 @@ class Rack::Attack
|
|
118
121
|
@cache ||= Cache.new
|
119
122
|
end
|
120
123
|
|
121
|
-
def
|
124
|
+
def clear_configuration
|
122
125
|
@safelists, @blocklists, @throttles, @tracks = {}, {}, {}, {}
|
123
126
|
@ip_blocklists = []
|
124
127
|
@ip_safelists = []
|
125
128
|
end
|
126
129
|
|
130
|
+
def clear!
|
131
|
+
warn "[DEPRECATION] Rack::Attack.clear! is deprecated. Please use Rack::Attack.clear_configuration instead"
|
132
|
+
clear_configuration
|
133
|
+
end
|
134
|
+
|
127
135
|
def blacklisted_response=(res)
|
128
136
|
warn "[DEPRECATION] 'Rack::Attack.blacklisted_response=' is deprecated. Please use 'blocklisted_response=' instead."
|
129
|
-
self.blocklisted_response=
|
137
|
+
self.blocklisted_response = res
|
130
138
|
end
|
131
139
|
|
132
140
|
def blacklisted_response
|
@@ -147,10 +155,10 @@ class Rack::Attack
|
|
147
155
|
|
148
156
|
# Set defaults
|
149
157
|
@notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
|
150
|
-
@blocklisted_response = lambda {|
|
151
|
-
@throttled_response = lambda {|env|
|
158
|
+
@blocklisted_response = lambda { |_env| [403, { 'Content-Type' => 'text/plain' }, ["Forbidden\n"]] }
|
159
|
+
@throttled_response = lambda { |env|
|
152
160
|
retry_after = (env['rack.attack.match_data'] || {})[:period]
|
153
|
-
[429, {'Content-Type' => 'text/plain', 'Retry-After' => retry_after.to_s}, ["Retry later\n"]]
|
161
|
+
[429, { 'Content-Type' => 'text/plain', 'Retry-After' => retry_after.to_s }, ["Retry later\n"]]
|
154
162
|
}
|
155
163
|
|
156
164
|
def initialize(app)
|
@@ -174,8 +182,5 @@ class Rack::Attack
|
|
174
182
|
end
|
175
183
|
|
176
184
|
extend Forwardable
|
177
|
-
def_delegators self, :safelisted?,
|
178
|
-
:blocklisted?,
|
179
|
-
:throttled?,
|
180
|
-
:tracked?
|
185
|
+
def_delegators self, :safelisted?, :blocklisted?, :throttled?, :tracked?
|
181
186
|
end
|
data/lib/rack/attack/cache.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
module Rack
|
2
2
|
class Attack
|
3
3
|
class Cache
|
4
|
-
|
5
4
|
attr_accessor :prefix
|
6
5
|
|
7
6
|
def initialize
|
@@ -43,7 +42,7 @@ module Rack
|
|
43
42
|
|
44
43
|
def key_and_expiry(unprefixed_key, period)
|
45
44
|
epoch_time = Time.now.to_i
|
46
|
-
# Add 1 to expires_in to avoid timing error:
|
45
|
+
# Add 1 to expires_in to avoid timing error: https://git.io/i1PHXA
|
47
46
|
expires_in = (period - (epoch_time % period) + 1).to_i
|
48
47
|
["#{prefix}:#{(epoch_time / period).to_i}:#{unprefixed_key}", expires_in]
|
49
48
|
end
|
data/lib/rack/attack/check.rb
CHANGED
data/lib/rack/attack/fail2ban.rb
CHANGED
@@ -27,6 +27,7 @@ module Rack
|
|
27
27
|
end
|
28
28
|
|
29
29
|
protected
|
30
|
+
|
30
31
|
def key_prefix
|
31
32
|
'fail2ban'
|
32
33
|
end
|
@@ -40,8 +41,8 @@ module Rack
|
|
40
41
|
true
|
41
42
|
end
|
42
43
|
|
43
|
-
|
44
44
|
private
|
45
|
+
|
45
46
|
def ban!(discriminator, bantime)
|
46
47
|
cache.write("#{key_prefix}:ban:#{discriminator}", 1, bantime)
|
47
48
|
end
|
@@ -1,8 +1,7 @@
|
|
1
1
|
class Rack::Attack
|
2
|
-
|
3
2
|
# When using Rack::Attack with a Rails app, developers expect the request path
|
4
3
|
# to be normalized. In particular, trailing slashes are stripped.
|
5
|
-
# (See
|
4
|
+
# (See https://git.io/v0rrR for implementation.)
|
6
5
|
#
|
7
6
|
# Look for an ActionDispatch utility class that Rails folks would expect
|
8
7
|
# to normalize request paths. If unavailable, use a fallback class that
|
@@ -15,10 +14,9 @@ class Rack::Attack
|
|
15
14
|
end
|
16
15
|
|
17
16
|
PathNormalizer = if defined?(::ActionDispatch::Journey::Router::Utils)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
17
|
+
# For Rails apps
|
18
|
+
::ActionDispatch::Journey::Router::Utils
|
19
|
+
else
|
20
|
+
FallbackPathNormalizer
|
21
|
+
end
|
24
22
|
end
|
data/lib/rack/attack/safelist.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
module Rack
|
2
2
|
class Attack
|
3
3
|
module StoreProxy
|
4
|
-
PROXIES = [DalliProxy, MemCacheProxy, RedisStoreProxy].freeze
|
4
|
+
PROXIES = [DalliProxy, MemCacheProxy, RedisStoreProxy, RedisCacheStoreProxy].freeze
|
5
5
|
|
6
|
-
ACTIVE_SUPPORT_WRAPPER_CLASSES = Set.new(['ActiveSupport::Cache::MemCacheStore', 'ActiveSupport::Cache::RedisStore']).freeze
|
6
|
+
ACTIVE_SUPPORT_WRAPPER_CLASSES = Set.new(['ActiveSupport::Cache::MemCacheStore', 'ActiveSupport::Cache::RedisStore', 'ActiveSupport::Cache::RedisCacheStore']).freeze
|
7
7
|
ACTIVE_SUPPORT_CLIENTS = Set.new(['Redis::Store', 'Dalli::Client', 'MemCache']).freeze
|
8
8
|
|
9
9
|
def self.build(store)
|
@@ -12,8 +12,6 @@ module Rack
|
|
12
12
|
klass ? klass.new(client) : client
|
13
13
|
end
|
14
14
|
|
15
|
-
|
16
|
-
private
|
17
15
|
def self.unwrap_active_support_stores(store)
|
18
16
|
# ActiveSupport::Cache::RedisStore doesn't expose any way to set an expiry,
|
19
17
|
# so use the raw Redis::Store instead.
|
@@ -28,14 +28,14 @@ module Rack
|
|
28
28
|
rescue Dalli::DalliError
|
29
29
|
end
|
30
30
|
|
31
|
-
def write(key, value, options={})
|
31
|
+
def write(key, value, options = {})
|
32
32
|
with do |client|
|
33
33
|
client.set(key, value, options.fetch(:expires_in, 0), raw: true)
|
34
34
|
end
|
35
35
|
rescue Dalli::DalliError
|
36
36
|
end
|
37
37
|
|
38
|
-
def increment(key, amount, options={})
|
38
|
+
def increment(key, amount, options = {})
|
39
39
|
with do |client|
|
40
40
|
client.incr(key, amount, options.fetch(:expires_in, 0), amount)
|
41
41
|
end
|
@@ -58,7 +58,6 @@ module Rack
|
|
58
58
|
end
|
59
59
|
end
|
60
60
|
end
|
61
|
-
|
62
61
|
end
|
63
62
|
end
|
64
63
|
end
|
@@ -14,21 +14,21 @@ module Rack
|
|
14
14
|
def read(key)
|
15
15
|
# Second argument: reading raw value
|
16
16
|
get(key, true)
|
17
|
-
|
17
|
+
rescue MemCache::MemCacheError
|
18
18
|
end
|
19
19
|
|
20
|
-
def write(key, value, options={})
|
20
|
+
def write(key, value, options = {})
|
21
21
|
# Third argument: writing raw value
|
22
22
|
set(key, value, options.fetch(:expires_in, 0), true)
|
23
23
|
rescue MemCache::MemCacheError
|
24
24
|
end
|
25
25
|
|
26
|
-
def increment(key, amount,
|
26
|
+
def increment(key, amount, _options = {})
|
27
27
|
incr(key, amount)
|
28
28
|
rescue MemCache::MemCacheError
|
29
29
|
end
|
30
30
|
|
31
|
-
def delete(key,
|
31
|
+
def delete(key, _options = {})
|
32
32
|
with do |client|
|
33
33
|
client.delete(key)
|
34
34
|
end
|
@@ -44,7 +44,6 @@ module Rack
|
|
44
44
|
end
|
45
45
|
end
|
46
46
|
end
|
47
|
-
|
48
47
|
end
|
49
48
|
end
|
50
49
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class Attack
|
5
|
+
module StoreProxy
|
6
|
+
class RedisCacheStoreProxy < SimpleDelegator
|
7
|
+
def self.handle?(store)
|
8
|
+
defined?(::ActiveSupport::Cache::RedisCacheStore) && store.is_a?(::ActiveSupport::Cache::RedisCacheStore)
|
9
|
+
end
|
10
|
+
|
11
|
+
def increment(name, amount, options = {})
|
12
|
+
# Redis doesn't check expiration on the INCRBY command. See https://redis.io/commands/expire
|
13
|
+
count = redis.pipelined do
|
14
|
+
redis.incrby(name, amount)
|
15
|
+
redis.expire(name, options[:expires_in]) if options[:expires_in]
|
16
|
+
end
|
17
|
+
count.first
|
18
|
+
end
|
19
|
+
|
20
|
+
def read(name, options = {})
|
21
|
+
super(name, options.merge!({ raw: true }))
|
22
|
+
end
|
23
|
+
|
24
|
+
def write(name, value, options = {})
|
25
|
+
super(name, value, options.merge!({ raw: true }))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -5,14 +5,7 @@ module Rack
|
|
5
5
|
module StoreProxy
|
6
6
|
class RedisStoreProxy < SimpleDelegator
|
7
7
|
def self.handle?(store)
|
8
|
-
|
9
|
-
#
|
10
|
-
# Go back to use defined? once this ruby issue is
|
11
|
-
# fixed and released:
|
12
|
-
# https://bugs.ruby-lang.org/issues/14407
|
13
|
-
#
|
14
|
-
# defined?(::Redis::Store) && store.is_a?(::Redis::Store)
|
15
|
-
const_defined?("::Redis::Store") && store.is_a?(::Redis::Store)
|
8
|
+
defined?(::Redis::Store) && store.is_a?(::Redis::Store)
|
16
9
|
end
|
17
10
|
|
18
11
|
def initialize(store)
|
@@ -24,7 +17,7 @@ module Rack
|
|
24
17
|
rescue Redis::BaseError
|
25
18
|
end
|
26
19
|
|
27
|
-
def write(key, value, options={})
|
20
|
+
def write(key, value, options = {})
|
28
21
|
if (expires_in = options[:expires_in])
|
29
22
|
setex(key, expires_in, value, raw: true)
|
30
23
|
else
|
@@ -33,7 +26,7 @@ module Rack
|
|
33
26
|
rescue Redis::BaseError
|
34
27
|
end
|
35
28
|
|
36
|
-
def increment(key, amount, options={})
|
29
|
+
def increment(key, amount, options = {})
|
37
30
|
count = nil
|
38
31
|
|
39
32
|
pipelined do
|
@@ -45,7 +38,7 @@ module Rack
|
|
45
38
|
rescue Redis::BaseError
|
46
39
|
end
|
47
40
|
|
48
|
-
def delete(key,
|
41
|
+
def delete(key, _options = {})
|
49
42
|
del(key)
|
50
43
|
rescue Redis::BaseError
|
51
44
|
end
|