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