rack-attack 5.2.0 → 5.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![Gem Version](https://badge.fury.io/rb/rack-attack.svg)](
|
9
|
+
[![Gem Version](https://badge.fury.io/rb/rack-attack.svg)](https://badge.fury.io/rb/rack-attack)
|
10
10
|
[![Build Status](https://travis-ci.org/kickstarter/rack-attack.svg?branch=master)](https://travis-ci.org/kickstarter/rack-attack)
|
11
11
|
[![Code Climate](https://codeclimate.com/github/kickstarter/rack-attack.svg)](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
|