rack-attack 5.0.1 → 5.1.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 +5 -5
- data/README.md +35 -13
- data/Rakefile +5 -2
- data/lib/rack/attack.rb +6 -3
- data/lib/rack/attack/allow2ban.rb +1 -1
- data/lib/rack/attack/cache.rb +11 -6
- data/lib/rack/attack/path_normalizer.rb +1 -4
- data/lib/rack/attack/store_proxy.rb +5 -2
- data/lib/rack/attack/store_proxy/redis_store_proxy.rb +17 -8
- data/lib/rack/attack/throttle.rb +2 -1
- data/lib/rack/attack/version.rb +1 -1
- data/spec/acceptance/blocking_spec.rb +21 -0
- data/spec/acceptance/safelisting_spec.rb +37 -0
- data/spec/acceptance/throttling_spec.rb +30 -0
- data/spec/allow2ban_spec.rb +3 -2
- data/spec/fail2ban_spec.rb +4 -2
- data/spec/integration/offline_spec.rb +0 -4
- data/spec/integration/rack_attack_cache_spec.rb +4 -5
- data/spec/rack_attack_request_spec.rb +1 -1
- data/spec/rack_attack_spec.rb +7 -8
- data/spec/rack_attack_store_config_spec.rb +20 -0
- data/spec/rack_attack_throttle_spec.rb +18 -8
- data/spec/rack_attack_track_spec.rb +4 -1
- data/spec/spec_helper.rb +9 -5
- metadata +73 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6adfaa8597fafd710ea09558be12046bf35eb3816a2cb81c275e07f12c3ebc9f
|
4
|
+
data.tar.gz: 9e256c9b94880a4118f47d3bec2826bdb1b0d6fb501567384cd3a8049b44fa86
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ab110884b8a75cec12ce734457a5e71f1f04351ad21f6ce160dbfda8161c642521e23c6d0d655dd03f51bafffecdff188cb89d88bf2b2e3b40e109bbf1ebffa2
|
7
|
+
data.tar.gz: c9e46912da6c457bc53970122c4cfc3afab4977037bf52c1dcd5ba9502ba6d83c0137b939d217645df347d2ac1cbec1b796f0d114bae2d0a668e4850dcc018b2
|
data/README.md
CHANGED
@@ -8,10 +8,14 @@ Throttle and fail2ban state is stored in a configurable cache (e.g. `Rails.cache
|
|
8
8
|
|
9
9
|
See the [Backing & Hacking blog post](http://www.kickstarter.com/backing-and-hacking/rack-attack-protection-from-abusive-clients) introducing Rack::Attack.
|
10
10
|
|
11
|
-
[](http://badge.fury.io/rb/rack-attack)
|
12
|
+
[](https://travis-ci.org/kickstarter/rack-attack)
|
13
|
+
[](https://codeclimate.com/github/kickstarter/rack-attack)
|
14
14
|
|
15
|
+
## Looking for maintainers
|
16
|
+
|
17
|
+
I'm looking for new maintainers to help me support Rack::Attack. Check out
|
18
|
+
[issue #219 for details](https://github.com/kickstarter/rack-attack/issues/219).
|
15
19
|
|
16
20
|
## Getting started
|
17
21
|
|
@@ -22,7 +26,7 @@ Install the [rack-attack](http://rubygems.org/gems/rack-attack) gem; or add it t
|
|
22
26
|
gem 'rack-attack'
|
23
27
|
```
|
24
28
|
Tell your app to use the Rack::Attack middleware.
|
25
|
-
For Rails
|
29
|
+
For Rails apps:
|
26
30
|
|
27
31
|
```ruby
|
28
32
|
# In config/application.rb
|
@@ -36,9 +40,9 @@ Or for Rackup files:
|
|
36
40
|
use Rack::Attack
|
37
41
|
```
|
38
42
|
|
39
|
-
Add a `
|
43
|
+
Add a `rack_attack.rb` file to `config/initializers/`:
|
40
44
|
```ruby
|
41
|
-
# In config/initializers/
|
45
|
+
# In config/initializers/rack_attack.rb
|
42
46
|
class Rack::Attack
|
43
47
|
# your custom configuration...
|
44
48
|
end
|
@@ -136,7 +140,7 @@ how the parameters work. For multiple filters, be sure to put each filter in a
|
|
136
140
|
Rack::Attack.blocklist('fail2ban pentesters') do |req|
|
137
141
|
# `filter` returns truthy value if request fails, or if it's from a previously banned IP
|
138
142
|
# so the request is blocked
|
139
|
-
Rack::Attack::Fail2Ban.filter("pentesters-#{req.ip}", :
|
143
|
+
Rack::Attack::Fail2Ban.filter("pentesters-#{req.ip}", maxretry: 3, findtime: 10.minutes, bantime: 5.minutes) do
|
140
144
|
# The count for the IP is incremented if the return value is truthy
|
141
145
|
CGI.unescape(req.query_string) =~ %r{/etc/passwd} ||
|
142
146
|
req.path.include?('/etc/passwd') ||
|
@@ -159,7 +163,7 @@ Rack::Attack.blocklist('allow2ban login scrapers') do |req|
|
|
159
163
|
# `filter` returns false value if request is to your login page (but still
|
160
164
|
# increments the count) so request below the limit are not blocked until
|
161
165
|
# they hit the limit. At that point, filter will return true and block.
|
162
|
-
Rack::Attack::Allow2Ban.filter(req.ip, :
|
166
|
+
Rack::Attack::Allow2Ban.filter(req.ip, maxretry: 20, findtime: 1.minute, bantime: 1.hour) do
|
163
167
|
# The count for the IP is incremented if the return value is truthy.
|
164
168
|
req.path == '/login' and req.post?
|
165
169
|
end
|
@@ -171,7 +175,7 @@ end
|
|
171
175
|
|
172
176
|
```ruby
|
173
177
|
# Throttle requests to 5 requests per second per ip
|
174
|
-
Rack::Attack.throttle('req/ip', :
|
178
|
+
Rack::Attack.throttle('req/ip', limit: 5, period: 1.second) do |req|
|
175
179
|
# If the return value is truthy, the cache key for the return value
|
176
180
|
# is incremented and compared with the limit. In this case:
|
177
181
|
# "rack::attack:#{Time.now.to_i/1.second}:req/ip:#{req.ip}"
|
@@ -183,7 +187,7 @@ end
|
|
183
187
|
|
184
188
|
# Throttle login attempts for a given email parameter to 6 reqs/minute
|
185
189
|
# Return the email as a discriminator on POST /login requests
|
186
|
-
Rack::Attack.throttle('logins/email', :
|
190
|
+
Rack::Attack.throttle('logins/email', limit: 6, period: 60) do |req|
|
187
191
|
req.params['email'] if req.path == '/login' && req.post?
|
188
192
|
end
|
189
193
|
|
@@ -191,7 +195,7 @@ end
|
|
191
195
|
# Rack::Auth::Basic has authenticated the user:
|
192
196
|
limit_proc = proc {|req| req.env["REMOTE_USER"] == "admin" ? 100 : 1}
|
193
197
|
period_proc = proc {|req| req.env["REMOTE_USER"] == "admin" ? 1.second : 1.minute}
|
194
|
-
Rack::Attack.throttle('req/ip', :
|
198
|
+
Rack::Attack.throttle('req/ip', limit: limit_proc, period: period_proc) do |req|
|
195
199
|
req.ip
|
196
200
|
end
|
197
201
|
```
|
@@ -205,7 +209,7 @@ Rack::Attack.track("special_agent") do |req|
|
|
205
209
|
end
|
206
210
|
|
207
211
|
# Supports optional limit and period, triggers the notification only when the limit is reached.
|
208
|
-
Rack::Attack.track("special_agent", :
|
212
|
+
Rack::Attack.track("special_agent", limit: 6, period: 60) do |req|
|
209
213
|
req.user_agent == "SpecialAgent"
|
210
214
|
end
|
211
215
|
|
@@ -233,7 +237,8 @@ Rack::Attack.throttled_response = lambda do |env|
|
|
233
237
|
# NB: you have access to the name and other data about the matched throttle
|
234
238
|
# env['rack.attack.matched'],
|
235
239
|
# env['rack.attack.match_type'],
|
236
|
-
# env['rack.attack.match_data']
|
240
|
+
# env['rack.attack.match_data'],
|
241
|
+
# env['rack.attack.match_discriminator']
|
237
242
|
|
238
243
|
# Using 503 because it may make attacker think that they have successfully
|
239
244
|
# DOSed the site. Rack::Attack returns 429 for throttling by default
|
@@ -316,6 +321,23 @@ Pull requests and issues are greatly appreciated. This project is intended to be
|
|
316
321
|
a safe, welcoming space for collaboration, and contributors are expected to
|
317
322
|
adhere to the [Code of Conduct](CODE_OF_CONDUCT.md).
|
318
323
|
|
324
|
+
### Testing pull requests
|
325
|
+
|
326
|
+
To run the minitest test suite, you will need both [Redis](http://redis.io/) and
|
327
|
+
[Memcached](https://memcached.org/) running locally and bound to IP `127.0.0.1` on
|
328
|
+
default ports (`6379` for Redis, and `11211` for Memcached) and able to be
|
329
|
+
accessed without authentication.
|
330
|
+
|
331
|
+
Install dependencies by running
|
332
|
+
```sh
|
333
|
+
bundle install
|
334
|
+
```
|
335
|
+
|
336
|
+
Then run the test suite by running
|
337
|
+
```sh
|
338
|
+
bundle exec rake
|
339
|
+
```
|
340
|
+
|
319
341
|
## Mailing list
|
320
342
|
|
321
343
|
New releases of Rack::Attack are announced on
|
data/Rakefile
CHANGED
@@ -10,11 +10,14 @@ namespace :test do
|
|
10
10
|
|
11
11
|
Rake::TestTask.new(:integration) do |t|
|
12
12
|
t.pattern = "spec/integration/*_spec.rb"
|
13
|
-
|
13
|
+
end
|
14
|
+
|
15
|
+
Rake::TestTask.new(:acceptance) do |t|
|
16
|
+
t.pattern = "spec/acceptance/*_spec.rb"
|
14
17
|
end
|
15
18
|
end
|
16
19
|
|
17
20
|
desc 'Run tests'
|
18
|
-
task :test => %w[test:units test:integration]
|
21
|
+
task :test => %w[test:units test:integration test:acceptance]
|
19
22
|
|
20
23
|
task :default => :test
|
data/lib/rack/attack.rb
CHANGED
@@ -2,11 +2,14 @@ require 'rack'
|
|
2
2
|
require 'forwardable'
|
3
3
|
|
4
4
|
class Rack::Attack
|
5
|
+
class MisconfiguredStoreError < StandardError; end
|
6
|
+
class MissingStoreError < StandardError; end
|
7
|
+
|
5
8
|
autoload :Cache, 'rack/attack/cache'
|
6
9
|
autoload :PathNormalizer, 'rack/attack/path_normalizer'
|
7
10
|
autoload :Check, 'rack/attack/check'
|
8
11
|
autoload :Throttle, 'rack/attack/throttle'
|
9
|
-
autoload :Safelist,
|
12
|
+
autoload :Safelist, 'rack/attack/safelist'
|
10
13
|
autoload :Blocklist, 'rack/attack/blocklist'
|
11
14
|
autoload :Track, 'rack/attack/track'
|
12
15
|
autoload :StoreProxy, 'rack/attack/store_proxy'
|
@@ -47,7 +50,7 @@ class Rack::Attack
|
|
47
50
|
self.tracks[name] = Track.new(name, options, block)
|
48
51
|
end
|
49
52
|
|
50
|
-
def safelists;
|
53
|
+
def safelists; @safelists ||= {}; end
|
51
54
|
def blocklists; @blocklists ||= {}; end
|
52
55
|
def throttles; @throttles ||= {}; end
|
53
56
|
def tracks; @tracks ||= {}; end
|
@@ -115,7 +118,7 @@ class Rack::Attack
|
|
115
118
|
|
116
119
|
def blacklisted_response
|
117
120
|
warn "[DEPRECATION] 'Rack::Attack.blacklisted_response' is deprecated. Please use 'blocklisted_response' instead."
|
118
|
-
|
121
|
+
blocklisted_response
|
119
122
|
end
|
120
123
|
|
121
124
|
end
|
@@ -7,7 +7,7 @@ module Rack
|
|
7
7
|
'allow2ban'
|
8
8
|
end
|
9
9
|
|
10
|
-
# everything the same here except we
|
10
|
+
# everything is the same here except we only return true
|
11
11
|
# (blocking the request) if they have tripped the limit.
|
12
12
|
def fail!(discriminator, bantime, findtime, maxretry)
|
13
13
|
count = cache.count("#{key_prefix}:count:#{discriminator}", findtime)
|
data/lib/rack/attack/cache.rb
CHANGED
@@ -46,15 +46,20 @@ module Rack
|
|
46
46
|
end
|
47
47
|
|
48
48
|
def do_count(key, expires_in)
|
49
|
-
|
49
|
+
if store.nil?
|
50
|
+
raise Rack::Attack::MissingStoreError
|
51
|
+
elsif !store.respond_to?(:increment)
|
52
|
+
raise Rack::Attack::MisconfiguredStoreError, "Store needs to respond to #increment"
|
53
|
+
else
|
54
|
+
result = store.increment(key, 1, :expires_in => expires_in)
|
50
55
|
|
51
|
-
|
52
|
-
|
53
|
-
|
56
|
+
# NB: Some stores return nil when incrementing uninitialized values
|
57
|
+
if result.nil?
|
58
|
+
store.write(key, 1, :expires_in => expires_in)
|
59
|
+
end
|
60
|
+
result || 1
|
54
61
|
end
|
55
|
-
result || 1
|
56
62
|
end
|
57
|
-
|
58
63
|
end
|
59
64
|
end
|
60
65
|
end
|
@@ -15,11 +15,8 @@ class Rack::Attack
|
|
15
15
|
end
|
16
16
|
|
17
17
|
PathNormalizer = if defined?(::ActionDispatch::Journey::Router::Utils)
|
18
|
-
# For Rails
|
18
|
+
# For Rails apps
|
19
19
|
::ActionDispatch::Journey::Router::Utils
|
20
|
-
elsif defined?(::Journey::Router::Utils)
|
21
|
-
# for Rails 3.2
|
22
|
-
::Journey::Router::Utils
|
23
20
|
else
|
24
21
|
FallbackPathNormalizer
|
25
22
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Rack
|
2
2
|
class Attack
|
3
3
|
module StoreProxy
|
4
|
-
PROXIES = [DalliProxy, MemCacheProxy, RedisStoreProxy]
|
4
|
+
PROXIES = [DalliProxy, MemCacheProxy, RedisStoreProxy].freeze
|
5
5
|
|
6
6
|
ACTIVE_SUPPORT_WRAPPER_CLASSES = Set.new(['ActiveSupport::Cache::MemCacheStore', 'ActiveSupport::Cache::RedisStore']).freeze
|
7
7
|
ACTIVE_SUPPORT_CLIENTS = Set.new(['Redis::Store', 'Dalli::Client', 'MemCache']).freeze
|
@@ -20,7 +20,10 @@ module Rack
|
|
20
20
|
# We also want to use the underlying Dalli client instead of ::ActiveSupport::Cache::MemCacheStore,
|
21
21
|
# and the MemCache client if using Rails 3.x
|
22
22
|
|
23
|
-
|
23
|
+
if store.instance_variable_defined?(:@data)
|
24
|
+
client = store.instance_variable_get(:@data)
|
25
|
+
end
|
26
|
+
|
24
27
|
if ACTIVE_SUPPORT_WRAPPER_CLASSES.include?(store.class.to_s) && ACTIVE_SUPPORT_CLIENTS.include?(client.class.to_s)
|
25
28
|
client
|
26
29
|
else
|
@@ -5,7 +5,14 @@ module Rack
|
|
5
5
|
module StoreProxy
|
6
6
|
class RedisStoreProxy < SimpleDelegator
|
7
7
|
def self.handle?(store)
|
8
|
-
|
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)
|
9
16
|
end
|
10
17
|
|
11
18
|
def initialize(store)
|
@@ -13,31 +20,33 @@ module Rack
|
|
13
20
|
end
|
14
21
|
|
15
22
|
def read(key)
|
16
|
-
|
23
|
+
get(key, raw: true)
|
17
24
|
rescue Redis::BaseError
|
18
25
|
end
|
19
26
|
|
20
27
|
def write(key, value, options={})
|
21
28
|
if (expires_in = options[:expires_in])
|
22
|
-
|
29
|
+
setex(key, expires_in, value, raw: true)
|
23
30
|
else
|
24
|
-
|
31
|
+
set(key, value, raw: true)
|
25
32
|
end
|
26
33
|
rescue Redis::BaseError
|
27
34
|
end
|
28
35
|
|
29
36
|
def increment(key, amount, options={})
|
30
37
|
count = nil
|
31
|
-
|
32
|
-
|
33
|
-
|
38
|
+
|
39
|
+
pipelined do
|
40
|
+
count = incrby(key, amount)
|
41
|
+
expire(key, options[:expires_in]) if options[:expires_in]
|
34
42
|
end
|
43
|
+
|
35
44
|
count.value if count
|
36
45
|
rescue Redis::BaseError
|
37
46
|
end
|
38
47
|
|
39
48
|
def delete(key, options={})
|
40
|
-
|
49
|
+
del(key)
|
41
50
|
rescue Redis::BaseError
|
42
51
|
end
|
43
52
|
end
|
data/lib/rack/attack/throttle.rb
CHANGED
data/lib/rack/attack/version.rb
CHANGED
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative "../spec_helper"
|
2
|
+
|
3
|
+
describe "#blocklist" do
|
4
|
+
before do
|
5
|
+
Rack::Attack.blocklist("block 1.2.3.4") do |request|
|
6
|
+
request.ip == "1.2.3.4"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
it "forbids request if blocklist condition is true" do
|
11
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
12
|
+
|
13
|
+
assert_equal 403, last_response.status
|
14
|
+
end
|
15
|
+
|
16
|
+
it "succeeds if blocklist condition is false" do
|
17
|
+
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
18
|
+
|
19
|
+
assert_equal 200, last_response.status
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative "../spec_helper"
|
2
|
+
|
3
|
+
describe "#safelist" do
|
4
|
+
before do
|
5
|
+
Rack::Attack.blocklist("block 1.2.3.4") do |request|
|
6
|
+
request.ip == "1.2.3.4"
|
7
|
+
end
|
8
|
+
|
9
|
+
Rack::Attack.safelist("safe path") do |request|
|
10
|
+
request.path == "/safe_space"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
it "forbids request if blocklist condition is true and safelist is false" do
|
15
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
16
|
+
|
17
|
+
assert_equal 403, last_response.status
|
18
|
+
end
|
19
|
+
|
20
|
+
it "succeeds if blocklist condition is false and safelist is false" do
|
21
|
+
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
22
|
+
|
23
|
+
assert_equal 200, last_response.status
|
24
|
+
end
|
25
|
+
|
26
|
+
it "succeeds request if blocklist condition is false and safelist is true" do
|
27
|
+
get "/safe_space", {}, "REMOTE_ADDR" => "5.6.7.8"
|
28
|
+
|
29
|
+
assert_equal 200, last_response.status
|
30
|
+
end
|
31
|
+
|
32
|
+
it "succeeds request if both blocklist and safelist conditions are true" do
|
33
|
+
get "/safe_space", {}, "REMOTE_ADDR" => "1.2.3.4"
|
34
|
+
|
35
|
+
assert_equal 200, last_response.status
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative "../spec_helper"
|
2
|
+
require "timecop"
|
3
|
+
|
4
|
+
describe "#throttle" do
|
5
|
+
it "allows one request per minute by IP" do
|
6
|
+
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
|
7
|
+
|
8
|
+
Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
|
9
|
+
request.ip
|
10
|
+
end
|
11
|
+
|
12
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
13
|
+
|
14
|
+
assert_equal 200, last_response.status
|
15
|
+
|
16
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
17
|
+
|
18
|
+
assert_equal 429, last_response.status
|
19
|
+
|
20
|
+
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
21
|
+
|
22
|
+
assert_equal 200, last_response.status
|
23
|
+
|
24
|
+
Timecop.travel(60) do
|
25
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
26
|
+
|
27
|
+
assert_equal 200, last_response.status
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/spec/allow2ban_spec.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require_relative 'spec_helper'
|
2
|
+
|
2
3
|
describe 'Rack::Attack.Allow2Ban' do
|
3
4
|
before do
|
4
5
|
# Use a long findtime; failures due to cache key rotation less likely
|
@@ -7,6 +8,7 @@ describe 'Rack::Attack.Allow2Ban' do
|
|
7
8
|
@bantime = 60
|
8
9
|
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
|
9
10
|
@f2b_options = {:bantime => @bantime, :findtime => @findtime, :maxretry => 2}
|
11
|
+
|
10
12
|
Rack::Attack.blocklist('pentest') do |req|
|
11
13
|
Rack::Attack::Allow2Ban.filter(req.ip, @f2b_options){req.query_string =~ /OMGHAX/}
|
12
14
|
end
|
@@ -23,6 +25,7 @@ describe 'Rack::Attack.Allow2Ban' do
|
|
23
25
|
describe 'making qualifying request' do
|
24
26
|
describe 'when not at maxretry' do
|
25
27
|
before { get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' }
|
28
|
+
|
26
29
|
it 'succeeds' do
|
27
30
|
last_response.status.must_equal 200
|
28
31
|
end
|
@@ -58,7 +61,6 @@ describe 'Rack::Attack.Allow2Ban' do
|
|
58
61
|
key = "rack::attack:allow2ban:ban:1.2.3.4"
|
59
62
|
@cache.store.read(key).must_equal 1
|
60
63
|
end
|
61
|
-
|
62
64
|
end
|
63
65
|
end
|
64
66
|
end
|
@@ -116,6 +118,5 @@ describe 'Rack::Attack.Allow2Ban' do
|
|
116
118
|
@cache.store.read(key).must_equal 1
|
117
119
|
end
|
118
120
|
end
|
119
|
-
|
120
121
|
end
|
121
122
|
end
|
data/spec/fail2ban_spec.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require_relative 'spec_helper'
|
2
|
+
|
2
3
|
describe 'Rack::Attack.Fail2Ban' do
|
3
4
|
before do
|
4
5
|
# Use a long findtime; failures due to cache key rotation less likely
|
@@ -7,6 +8,7 @@ describe 'Rack::Attack.Fail2Ban' do
|
|
7
8
|
@bantime = 60
|
8
9
|
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
|
9
10
|
@f2b_options = {:bantime => @bantime, :findtime => @findtime, :maxretry => 2}
|
11
|
+
|
10
12
|
Rack::Attack.blocklist('pentest') do |req|
|
11
13
|
Rack::Attack::Fail2Ban.filter(req.ip, @f2b_options){req.query_string =~ /OMGHAX/}
|
12
14
|
end
|
@@ -23,6 +25,7 @@ describe 'Rack::Attack.Fail2Ban' do
|
|
23
25
|
describe 'making failing request' do
|
24
26
|
describe 'when not at maxretry' do
|
25
27
|
before { get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' }
|
28
|
+
|
26
29
|
it 'fails' do
|
27
30
|
last_response.status.must_equal 403
|
28
31
|
end
|
@@ -73,7 +76,7 @@ describe 'Rack::Attack.Fail2Ban' do
|
|
73
76
|
|
74
77
|
it 'resets fail count' do
|
75
78
|
key = "rack::attack:#{Time.now.to_i/@findtime}:fail2ban:count:1.2.3.4"
|
76
|
-
@cache.store.read(key)
|
79
|
+
assert_nil @cache.store.read(key)
|
77
80
|
end
|
78
81
|
|
79
82
|
it 'IP is not banned' do
|
@@ -136,6 +139,5 @@ describe 'Rack::Attack.Fail2Ban' do
|
|
136
139
|
@cache.store.read(key).must_equal 1
|
137
140
|
end
|
138
141
|
end
|
139
|
-
|
140
142
|
end
|
141
143
|
end
|
@@ -4,7 +4,6 @@ require 'dalli'
|
|
4
4
|
require_relative '../spec_helper'
|
5
5
|
|
6
6
|
OfflineExamples = Minitest::SharedExamples.new do
|
7
|
-
|
8
7
|
it 'should write' do
|
9
8
|
@cache.write('cache-test-key', 'foobar', 1)
|
10
9
|
end
|
@@ -16,7 +15,6 @@ OfflineExamples = Minitest::SharedExamples.new do
|
|
16
15
|
it 'should count' do
|
17
16
|
@cache.send(:do_count, 'rack::attack::cache-test-key', 1)
|
18
17
|
end
|
19
|
-
|
20
18
|
end
|
21
19
|
|
22
20
|
describe 'when Redis is offline' do
|
@@ -27,7 +25,6 @@ describe 'when Redis is offline' do
|
|
27
25
|
# Use presumably unused port for Redis client
|
28
26
|
@cache.store = ActiveSupport::Cache::RedisStore.new(:host => '127.0.0.1', :port => 3333)
|
29
27
|
}
|
30
|
-
|
31
28
|
end
|
32
29
|
|
33
30
|
describe 'when Memcached is offline' do
|
@@ -43,5 +40,4 @@ describe 'when Memcached is offline' do
|
|
43
40
|
after {
|
44
41
|
Dalli.logger.level = Logger::INFO
|
45
42
|
}
|
46
|
-
|
47
43
|
end
|
@@ -1,7 +1,6 @@
|
|
1
1
|
require_relative '../spec_helper'
|
2
2
|
|
3
3
|
describe Rack::Attack::Cache do
|
4
|
-
|
5
4
|
# A convenience method for deleting a key from cache.
|
6
5
|
# Slightly differnet than @cache.delete, which adds a prefix.
|
7
6
|
def delete(key)
|
@@ -20,6 +19,7 @@ describe Rack::Attack::Cache do
|
|
20
19
|
require 'active_support/cache/mem_cache_store'
|
21
20
|
require 'active_support/cache/redis_store'
|
22
21
|
require 'connection_pool'
|
22
|
+
|
23
23
|
cache_stores = [
|
24
24
|
ActiveSupport::Cache::MemoryStore.new,
|
25
25
|
ActiveSupport::Cache::DalliStore.new("127.0.0.1"),
|
@@ -32,8 +32,8 @@ describe Rack::Attack::Cache do
|
|
32
32
|
|
33
33
|
cache_stores.each do |store|
|
34
34
|
store = Rack::Attack::StoreProxy.build(store)
|
35
|
-
describe "with #{store.class}" do
|
36
35
|
|
36
|
+
describe "with #{store.class}" do
|
37
37
|
before {
|
38
38
|
@cache = Rack::Attack::Cache.new
|
39
39
|
@key = "rack::attack:cache-test-key"
|
@@ -92,7 +92,7 @@ describe Rack::Attack::Cache do
|
|
92
92
|
store.write(@key, "foobar", :expires_in => @expires_in)
|
93
93
|
@cache.read('cache-test-key').must_equal "foobar"
|
94
94
|
store.delete(@key)
|
95
|
-
@cache.read('cache-test-key')
|
95
|
+
assert_nil @cache.read('cache-test-key')
|
96
96
|
end
|
97
97
|
end
|
98
98
|
|
@@ -113,10 +113,9 @@ describe Rack::Attack::Cache do
|
|
113
113
|
period_key, _ = @cache.send(:key_and_expiry, 'cache-test-key', period)
|
114
114
|
store.read(period_key).to_i.must_equal 1
|
115
115
|
@cache.reset_count(unprefixed_key, period)
|
116
|
-
store.read(period_key)
|
116
|
+
assert_nil store.read(period_key)
|
117
117
|
end
|
118
118
|
end
|
119
119
|
end
|
120
|
-
|
121
120
|
end
|
122
121
|
end
|
data/spec/rack_attack_spec.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require_relative 'spec_helper'
|
2
2
|
|
3
3
|
describe 'Rack::Attack' do
|
4
|
-
|
4
|
+
it_allows_ok_requests
|
5
5
|
|
6
6
|
describe 'normalizing paths' do
|
7
7
|
before do
|
@@ -33,17 +33,18 @@ describe 'Rack::Attack' do
|
|
33
33
|
|
34
34
|
describe "a bad request" do
|
35
35
|
before { get '/', {}, 'REMOTE_ADDR' => @bad_ip }
|
36
|
+
|
36
37
|
it "should return a blocklist response" do
|
37
|
-
get '/', {}, 'REMOTE_ADDR' => @bad_ip
|
38
38
|
last_response.status.must_equal 403
|
39
39
|
last_response.body.must_equal "Forbidden\n"
|
40
40
|
end
|
41
|
+
|
41
42
|
it "should tag the env" do
|
42
43
|
last_request.env['rack.attack.matched'].must_equal "ip #{@bad_ip}"
|
43
44
|
last_request.env['rack.attack.match_type'].must_equal :blocklist
|
44
45
|
end
|
45
46
|
|
46
|
-
|
47
|
+
it_allows_ok_requests
|
47
48
|
end
|
48
49
|
|
49
50
|
describe "and safelist" do
|
@@ -52,7 +53,7 @@ describe 'Rack::Attack' do
|
|
52
53
|
Rack::Attack.safelist("good ua") {|req| req.user_agent == @good_ua }
|
53
54
|
end
|
54
55
|
|
55
|
-
it('has a safelist'){ Rack::Attack.safelists.key?("good ua") }
|
56
|
+
it('has a safelist') { Rack::Attack.safelists.key?("good ua") }
|
56
57
|
|
57
58
|
it('has a whitelist with a deprication warning') {
|
58
59
|
_, stderror = capture_io do
|
@@ -63,10 +64,11 @@ describe 'Rack::Attack' do
|
|
63
64
|
|
64
65
|
describe "with a request match both safelist & blocklist" do
|
65
66
|
before { get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua }
|
67
|
+
|
66
68
|
it "should allow safelists before blocklists" do
|
67
|
-
get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua
|
68
69
|
last_response.status.must_equal 200
|
69
70
|
end
|
71
|
+
|
70
72
|
it "should tag the env" do
|
71
73
|
last_request.env['rack.attack.matched'].must_equal 'good ua'
|
72
74
|
last_request.env['rack.attack.match_type'].must_equal :safelist
|
@@ -84,7 +86,6 @@ describe 'Rack::Attack' do
|
|
84
86
|
Rack::Attack.blacklisted_response
|
85
87
|
end
|
86
88
|
assert_match "[DEPRECATION] 'Rack::Attack.blacklisted_response' is deprecated. Please use 'blocklisted_response' instead.", stderror
|
87
|
-
|
88
89
|
end
|
89
90
|
end
|
90
91
|
|
@@ -93,7 +94,5 @@ describe 'Rack::Attack' do
|
|
93
94
|
Rack::Attack.throttled_response.must_respond_to :call
|
94
95
|
end
|
95
96
|
end
|
96
|
-
|
97
97
|
end
|
98
|
-
|
99
98
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Store configuration' do
|
4
|
+
it "gives clear error when store it's not configured if it's needed" do
|
5
|
+
Rack::Attack.throttle('ip/sec', limit: 1, period: 60) { |req| req.ip }
|
6
|
+
|
7
|
+
assert_raises(Rack::Attack::MissingStoreError) do
|
8
|
+
get '/'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
it "gives clear error when store isn't configured properly" do
|
13
|
+
Rack::Attack.cache.store = Object.new
|
14
|
+
Rack::Attack.throttle('ip/sec', limit: 1, period: 60) { |req| req.ip }
|
15
|
+
|
16
|
+
assert_raises(Rack::Attack::MisconfiguredStoreError) do
|
17
|
+
get '/'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require_relative 'spec_helper'
|
2
|
+
|
2
3
|
describe 'Rack::Attack.throttle' do
|
3
4
|
before do
|
4
5
|
@period = 60 # Use a long period; failures due to cache key rotation less likely
|
@@ -6,11 +7,13 @@ describe 'Rack::Attack.throttle' do
|
|
6
7
|
Rack::Attack.throttle('ip/sec', :limit => 1, :period => @period) { |req| req.ip }
|
7
8
|
end
|
8
9
|
|
9
|
-
it('should have a throttle'){ Rack::Attack.throttles.key?('ip/sec') }
|
10
|
-
|
10
|
+
it('should have a throttle') { Rack::Attack.throttles.key?('ip/sec') }
|
11
|
+
|
12
|
+
it_allows_ok_requests
|
11
13
|
|
12
14
|
describe 'a single request' do
|
13
15
|
before { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
|
16
|
+
|
14
17
|
it 'should set the counter for one request' do
|
15
18
|
key = "rack::attack:#{Time.now.to_i/@period}:ip/sec:1.2.3.4"
|
16
19
|
Rack::Attack.cache.store.read(key).must_equal 1
|
@@ -21,19 +24,23 @@ describe 'Rack::Attack.throttle' do
|
|
21
24
|
last_request.env['rack.attack.throttle_data']['ip/sec'].must_equal data
|
22
25
|
end
|
23
26
|
end
|
27
|
+
|
24
28
|
describe "with 2 requests" do
|
25
29
|
before do
|
26
30
|
2.times { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
|
27
31
|
end
|
32
|
+
|
28
33
|
it 'should block the last request' do
|
29
34
|
last_response.status.must_equal 429
|
30
35
|
end
|
36
|
+
|
31
37
|
it 'should tag the env' do
|
32
38
|
last_request.env['rack.attack.matched'].must_equal 'ip/sec'
|
33
39
|
last_request.env['rack.attack.match_type'].must_equal :throttle
|
34
40
|
last_request.env['rack.attack.match_data'].must_equal({:count => 2, :limit => 1, :period => @period})
|
35
41
|
last_request.env['rack.attack.match_discriminator'].must_equal('1.2.3.4')
|
36
42
|
end
|
43
|
+
|
37
44
|
it 'should set a Retry-After header' do
|
38
45
|
last_response.headers['Retry-After'].must_equal @period.to_s
|
39
46
|
end
|
@@ -47,10 +54,11 @@ describe 'Rack::Attack.throttle with limit as proc' do
|
|
47
54
|
Rack::Attack.throttle('ip/sec', :limit => lambda { |req| 1 }, :period => @period) { |req| req.ip }
|
48
55
|
end
|
49
56
|
|
50
|
-
|
57
|
+
it_allows_ok_requests
|
51
58
|
|
52
59
|
describe 'a single request' do
|
53
60
|
before { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
|
61
|
+
|
54
62
|
it 'should set the counter for one request' do
|
55
63
|
key = "rack::attack:#{Time.now.to_i/@period}:ip/sec:1.2.3.4"
|
56
64
|
Rack::Attack.cache.store.read(key).must_equal 1
|
@@ -70,10 +78,11 @@ describe 'Rack::Attack.throttle with period as proc' do
|
|
70
78
|
Rack::Attack.throttle('ip/sec', :limit => lambda { |req| 1 }, :period => lambda { |req| @period }) { |req| req.ip }
|
71
79
|
end
|
72
80
|
|
73
|
-
|
81
|
+
it_allows_ok_requests
|
74
82
|
|
75
83
|
describe 'a single request' do
|
76
84
|
before { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
|
85
|
+
|
77
86
|
it 'should set the counter for one request' do
|
78
87
|
key = "rack::attack:#{Time.now.to_i/@period}:ip/sec:1.2.3.4"
|
79
88
|
Rack::Attack.cache.store.read(key).must_equal 1
|
@@ -93,17 +102,18 @@ describe 'Rack::Attack.throttle with block retuning nil' do
|
|
93
102
|
Rack::Attack.throttle('ip/sec', :limit => 1, :period => @period) { |_| nil }
|
94
103
|
end
|
95
104
|
|
96
|
-
|
105
|
+
it_allows_ok_requests
|
97
106
|
|
98
107
|
describe 'a single request' do
|
99
108
|
before { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
|
109
|
+
|
100
110
|
it 'should not set the counter' do
|
101
111
|
key = "rack::attack:#{Time.now.to_i/@period}:ip/sec:1.2.3.4"
|
102
|
-
Rack::Attack.cache.store.read(key)
|
112
|
+
assert_nil Rack::Attack.cache.store.read(key)
|
103
113
|
end
|
104
114
|
|
105
115
|
it 'should not populate throttle data' do
|
106
|
-
last_request.env['rack.attack.throttle_data']
|
116
|
+
assert_nil last_request.env['rack.attack.throttle_data']
|
107
117
|
end
|
108
118
|
end
|
109
|
-
end
|
119
|
+
end
|
@@ -18,7 +18,9 @@ describe 'Rack::Attack.track' do
|
|
18
18
|
before do
|
19
19
|
Rack::Attack.track("everything"){ |req| true }
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
|
+
it_allows_ok_requests
|
23
|
+
|
22
24
|
it "should tag the env" do
|
23
25
|
get '/'
|
24
26
|
last_request.env['rack.attack.matched'].must_equal 'everything'
|
@@ -34,6 +36,7 @@ describe 'Rack::Attack.track' do
|
|
34
36
|
ActiveSupport::Notifications.subscribe("rack.attack") do |*args|
|
35
37
|
Counter.incr
|
36
38
|
end
|
39
|
+
|
37
40
|
get "/"
|
38
41
|
end
|
39
42
|
|
data/spec/spec_helper.rb
CHANGED
@@ -7,9 +7,6 @@ require "rack/test"
|
|
7
7
|
require 'active_support'
|
8
8
|
require 'action_dispatch'
|
9
9
|
|
10
|
-
# Load Journey for Rails 3.2
|
11
|
-
require 'journey' if ActionPack::VERSION::MAJOR == 3
|
12
|
-
|
13
10
|
require "rack/attack"
|
14
11
|
|
15
12
|
begin
|
@@ -22,16 +19,23 @@ class MiniTest::Spec
|
|
22
19
|
|
23
20
|
include Rack::Test::Methods
|
24
21
|
|
25
|
-
after
|
22
|
+
after do
|
23
|
+
Rack::Attack.clear!
|
24
|
+
Rack::Attack.instance_variable_set(:@cache, nil)
|
25
|
+
end
|
26
26
|
|
27
27
|
def app
|
28
28
|
Rack::Builder.new {
|
29
|
+
# Use Rack::Lint to test that rack-attack is complying with the rack spec
|
30
|
+
use Rack::Lint
|
29
31
|
use Rack::Attack
|
32
|
+
use Rack::Lint
|
33
|
+
|
30
34
|
run lambda {|env| [200, {}, ['Hello World']]}
|
31
35
|
}.to_app
|
32
36
|
end
|
33
37
|
|
34
|
-
def self.
|
38
|
+
def self.it_allows_ok_requests
|
35
39
|
it "must allow ok requests" do
|
36
40
|
get '/', {}, 'REMOTE_ADDR' => '127.0.0.1'
|
37
41
|
last_response.status.must_equal 200
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-attack
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.0
|
4
|
+
version: 5.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aaron Suggs
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-03-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -164,6 +164,62 @@ dependencies:
|
|
164
164
|
- - ">="
|
165
165
|
- !ruby/object:Gem::Version
|
166
166
|
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: timecop
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: pry
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - ">="
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '0'
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: guard-minitest
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - ">="
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '0'
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - ">="
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '0'
|
209
|
+
- !ruby/object:Gem::Dependency
|
210
|
+
name: guard
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - ">="
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: '0'
|
216
|
+
type: :development
|
217
|
+
prerelease: false
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
219
|
+
requirements:
|
220
|
+
- - ">="
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: '0'
|
167
223
|
description: A rack middleware for throttling and blocking abusive requests
|
168
224
|
email: aaron@ktheory.com
|
169
225
|
executables: []
|
@@ -188,6 +244,9 @@ files:
|
|
188
244
|
- lib/rack/attack/throttle.rb
|
189
245
|
- lib/rack/attack/track.rb
|
190
246
|
- lib/rack/attack/version.rb
|
247
|
+
- spec/acceptance/blocking_spec.rb
|
248
|
+
- spec/acceptance/safelisting_spec.rb
|
249
|
+
- spec/acceptance/throttling_spec.rb
|
191
250
|
- spec/allow2ban_spec.rb
|
192
251
|
- spec/fail2ban_spec.rb
|
193
252
|
- spec/integration/offline_spec.rb
|
@@ -196,6 +255,7 @@ files:
|
|
196
255
|
- spec/rack_attack_path_normalizer_spec.rb
|
197
256
|
- spec/rack_attack_request_spec.rb
|
198
257
|
- spec/rack_attack_spec.rb
|
258
|
+
- spec/rack_attack_store_config_spec.rb
|
199
259
|
- spec/rack_attack_throttle_spec.rb
|
200
260
|
- spec/rack_attack_track_spec.rb
|
201
261
|
- spec/spec_helper.rb
|
@@ -220,19 +280,23 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
220
280
|
version: '0'
|
221
281
|
requirements: []
|
222
282
|
rubyforge_project:
|
223
|
-
rubygems_version: 2.
|
283
|
+
rubygems_version: 2.7.3
|
224
284
|
signing_key:
|
225
285
|
specification_version: 4
|
226
286
|
summary: Block & throttle abusive requests
|
227
287
|
test_files:
|
228
|
-
- spec/
|
229
|
-
- spec/
|
288
|
+
- spec/spec_helper.rb
|
289
|
+
- spec/rack_attack_store_config_spec.rb
|
290
|
+
- spec/rack_attack_throttle_spec.rb
|
291
|
+
- spec/rack_attack_spec.rb
|
230
292
|
- spec/integration/offline_spec.rb
|
231
293
|
- spec/integration/rack_attack_cache_spec.rb
|
294
|
+
- spec/rack_attack_track_spec.rb
|
295
|
+
- spec/fail2ban_spec.rb
|
232
296
|
- spec/rack_attack_dalli_proxy_spec.rb
|
233
297
|
- spec/rack_attack_path_normalizer_spec.rb
|
298
|
+
- spec/acceptance/throttling_spec.rb
|
299
|
+
- spec/acceptance/blocking_spec.rb
|
300
|
+
- spec/acceptance/safelisting_spec.rb
|
234
301
|
- spec/rack_attack_request_spec.rb
|
235
|
-
- spec/
|
236
|
-
- spec/rack_attack_throttle_spec.rb
|
237
|
-
- spec/rack_attack_track_spec.rb
|
238
|
-
- spec/spec_helper.rb
|
302
|
+
- spec/allow2ban_spec.rb
|