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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -24
  3. data/Rakefile +1 -1
  4. data/lib/rack/attack.rb +28 -23
  5. data/lib/rack/attack/allow2ban.rb +1 -0
  6. data/lib/rack/attack/blocklist.rb +0 -1
  7. data/lib/rack/attack/cache.rb +1 -2
  8. data/lib/rack/attack/check.rb +1 -2
  9. data/lib/rack/attack/fail2ban.rb +2 -1
  10. data/lib/rack/attack/path_normalizer.rb +6 -8
  11. data/lib/rack/attack/safelist.rb +0 -1
  12. data/lib/rack/attack/store_proxy.rb +2 -4
  13. data/lib/rack/attack/store_proxy/dalli_proxy.rb +2 -3
  14. data/lib/rack/attack/store_proxy/mem_cache_proxy.rb +4 -5
  15. data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +30 -0
  16. data/lib/rack/attack/store_proxy/redis_store_proxy.rb +4 -11
  17. data/lib/rack/attack/version.rb +1 -1
  18. data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +2 -2
  19. data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +2 -2
  20. data/spec/acceptance/cache_store_config_for_throttle_spec.rb +1 -1
  21. data/spec/acceptance/customizing_blocked_response_spec.rb +1 -1
  22. data/spec/acceptance/customizing_throttled_response_spec.rb +1 -1
  23. data/spec/acceptance/safelisting_ip_spec.rb +0 -1
  24. data/spec/acceptance/stores/mem_cache_store_spec.rb +38 -0
  25. data/spec/acceptance/stores/redis_cache_store_spec.rb +41 -0
  26. data/spec/allow2ban_spec.rb +6 -6
  27. data/spec/fail2ban_spec.rb +7 -7
  28. data/spec/integration/rack_attack_cache_spec.rb +4 -1
  29. data/spec/rack_attack_dalli_proxy_spec.rb +0 -2
  30. data/spec/rack_attack_spec.rb +6 -6
  31. data/spec/rack_attack_throttle_spec.rb +7 -7
  32. data/spec/rack_attack_track_spec.rb +5 -5
  33. data/spec/spec_helper.rb +3 -4
  34. data/spec/support/cache_store_helper.rb +58 -0
  35. metadata +65 -44
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21a52aca7aa6592b23e6a3e99f0b06cf6d4d9eedb8366ec57fe4f9cfe804ea82
4
- data.tar.gz: 32e0db149bc10308fb8b5ae737147e1c11c9f854e08b9f453d9a7066911308fe
3
+ metadata.gz: c8f4c760b1ff68e6fbe79cf7576cbe2e0cc79ef5e2734ff55e63b46665ed6731
4
+ data.tar.gz: 75d7dd7fc98035d3961952582373af467fcfd225dac11af8db4aaf1cbaaf44b8
5
5
  SHA512:
6
- metadata.gz: 560d951d375a9114752b37a2858f48e85af9edcdeabb0073c6e6f8d179b6d10a0331a7abee625df4c0171f9c993bb105cc8e54fed9170fd47d288fb2bf29617a
7
- data.tar.gz: b3993951744e2755873cc7ce0c6810c3780ad44c6c84491bda5277a46536589d15b26bd25e7849e7b20a4e28aebc86ed874cb0b61e0d988da54252a3b3e024dc
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](http://www.kickstarter.com/backing-and-hacking/rack-attack-protection-from-abusive-clients) introducing Rack::Attack.
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)](http://badge.fury.io/rb/rack-attack)
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 returna a truthy value if you want the request to be blocked, and falsy otherwise.
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](http://www.fail2ban.org/wiki/index.php/Main_Page).
159
- See the [fail2ban documentation](http://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jail_Options) for more details on
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://rack.rubyforge.org/doc/SPEC.html).
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](http://nginx.org/en/docs/http/ngx_http_limit_conn_module.html#limit_conn_zone).
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
- Pull requests and issues are greatly appreciated. This project is intended to be
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
- ### Testing pull requests
407
+ ## Code of Conduct
410
408
 
411
- To run the minitest test suite, you will need both [Redis](http://redis.io/) and
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
- Install dependencies by running
417
- ```sh
418
- bundle install
419
- ```
411
+ ## Development setup
420
412
 
421
- Then run the test suite by running
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](http://opensource.org/licenses/MIT).
426
+ Released under an [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -13,7 +13,7 @@ namespace :test do
13
13
  end
14
14
 
15
15
  Rake::TestTask.new(:acceptance) do |t|
16
- t.pattern = "spec/acceptance/*_spec.rb"
16
+ t.pattern = "spec/acceptance/**/*_spec.rb"
17
17
  end
18
18
  end
19
19
 
@@ -8,21 +8,21 @@ class Rack::Attack
8
8
  class MisconfiguredStoreError < StandardError; end
9
9
  class MissingStoreError < StandardError; end
10
10
 
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 :Fail2Ban, 'rack/attack/fail2ban'
22
- autoload :Allow2Ban, 'rack/attack/allow2ban'
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 |name, throttle|
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 clear!
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=(res)
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 {|env| [403, {'Content-Type' => 'text/plain'}, ["Forbidden\n"]] }
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
@@ -3,6 +3,7 @@ module Rack
3
3
  class Allow2Ban < Fail2Ban
4
4
  class << self
5
5
  protected
6
+
6
7
  def key_prefix
7
8
  'allow2ban'
8
9
  end
@@ -5,7 +5,6 @@ module Rack
5
5
  super
6
6
  @type = :blocklist
7
7
  end
8
-
9
8
  end
10
9
  end
11
10
  end
@@ -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: http://git.io/i1PHXA
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
@@ -8,7 +8,7 @@ module Rack
8
8
  end
9
9
 
10
10
  def [](req)
11
- block[req].tap {|match|
11
+ block[req].tap { |match|
12
12
  if match
13
13
  req.env["rack.attack.matched"] = name
14
14
  req.env["rack.attack.match_type"] = type
@@ -21,4 +21,3 @@ module Rack
21
21
  end
22
22
  end
23
23
  end
24
-
@@ -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 http://git.io/v0rrR for implementation.)
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
- # For Rails apps
19
- ::ActionDispatch::Journey::Router::Utils
20
- else
21
- FallbackPathNormalizer
22
- end
23
-
17
+ # For Rails apps
18
+ ::ActionDispatch::Journey::Router::Utils
19
+ else
20
+ FallbackPathNormalizer
21
+ end
24
22
  end
@@ -5,7 +5,6 @@ module Rack
5
5
  super
6
6
  @type = :safelist
7
7
  end
8
-
9
8
  end
10
9
  end
11
10
  end
@@ -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
- rescue MemCache::MemCacheError
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, options={})
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, options={})
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
- # Using const_defined? for now.
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, options={})
41
+ def delete(key, _options = {})
49
42
  del(key)
50
43
  rescue Redis::BaseError
51
44
  end