rack-attack 6.0.0 → 6.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 +19 -5
- data/lib/rack/attack.rb +97 -146
- data/lib/rack/attack/cache.rb +15 -1
- data/lib/rack/attack/check.rb +2 -1
- data/lib/rack/attack/configuration.rb +107 -0
- data/lib/rack/attack/path_normalizer.rb +20 -18
- data/lib/rack/attack/railtie.rb +13 -0
- data/lib/rack/attack/store_proxy/active_support_redis_store_proxy.rb +3 -1
- data/lib/rack/attack/store_proxy/mem_cache_store_proxy.rb +3 -1
- data/lib/rack/attack/store_proxy/redis_proxy.rb +16 -7
- data/lib/rack/attack/throttle.rb +32 -14
- data/lib/rack/attack/track.rb +6 -5
- data/lib/rack/attack/version.rb +1 -1
- data/spec/acceptance/rails_middleware_spec.rb +35 -0
- data/spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb +1 -3
- data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +7 -1
- data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +6 -1
- data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +3 -3
- data/spec/acceptance/throttling_spec.rb +19 -1
- data/spec/allow2ban_spec.rb +17 -14
- data/spec/fail2ban_spec.rb +17 -16
- data/spec/integration/offline_spec.rb +46 -1
- data/spec/rack_attack_instrumentation_spec.rb +1 -1
- data/spec/rack_attack_path_normalizer_spec.rb +2 -2
- data/spec/rack_attack_spec.rb +58 -13
- data/spec/rack_attack_throttle_spec.rb +43 -18
- data/spec/rack_attack_track_spec.rb +8 -5
- data/spec/spec_helper.rb +7 -9
- metadata +31 -21
data/spec/fail2ban_spec.rb
CHANGED
@@ -20,7 +20,7 @@ describe 'Rack::Attack.Fail2Ban' do
|
|
20
20
|
describe 'making ok request' do
|
21
21
|
it 'succeeds' do
|
22
22
|
get '/', {}, 'REMOTE_ADDR' => '1.2.3.4'
|
23
|
-
last_response.status.must_equal 200
|
23
|
+
_(last_response.status).must_equal 200
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
@@ -29,17 +29,17 @@ describe 'Rack::Attack.Fail2Ban' do
|
|
29
29
|
before { get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' }
|
30
30
|
|
31
31
|
it 'fails' do
|
32
|
-
last_response.status.must_equal 403
|
32
|
+
_(last_response.status).must_equal 403
|
33
33
|
end
|
34
34
|
|
35
35
|
it 'increases fail count' do
|
36
36
|
key = "rack::attack:#{Time.now.to_i / @findtime}:fail2ban:count:1.2.3.4"
|
37
|
-
@cache.store.read(key).must_equal 1
|
37
|
+
_(@cache.store.read(key)).must_equal 1
|
38
38
|
end
|
39
39
|
|
40
40
|
it 'is not banned' do
|
41
41
|
key = "rack::attack:fail2ban:1.2.3.4"
|
42
|
-
@cache.store.read(key).must_be_nil
|
42
|
+
_(@cache.store.read(key)).must_be_nil
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
@@ -51,17 +51,17 @@ describe 'Rack::Attack.Fail2Ban' do
|
|
51
51
|
end
|
52
52
|
|
53
53
|
it 'fails' do
|
54
|
-
last_response.status.must_equal 403
|
54
|
+
_(last_response.status).must_equal 403
|
55
55
|
end
|
56
56
|
|
57
57
|
it 'increases fail count' do
|
58
58
|
key = "rack::attack:#{Time.now.to_i / @findtime}:fail2ban:count:1.2.3.4"
|
59
|
-
@cache.store.read(key).must_equal 2
|
59
|
+
_(@cache.store.read(key)).must_equal 2
|
60
60
|
end
|
61
61
|
|
62
62
|
it 'is banned' do
|
63
63
|
key = "rack::attack:fail2ban:ban:1.2.3.4"
|
64
|
-
@cache.store.read(key).must_equal 1
|
64
|
+
_(@cache.store.read(key)).must_equal 1
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
@@ -73,7 +73,7 @@ describe 'Rack::Attack.Fail2Ban' do
|
|
73
73
|
end
|
74
74
|
|
75
75
|
it 'succeeds' do
|
76
|
-
last_response.status.must_equal 200
|
76
|
+
_(last_response.status).must_equal 200
|
77
77
|
end
|
78
78
|
|
79
79
|
it 'resets fail count' do
|
@@ -82,7 +82,7 @@ describe 'Rack::Attack.Fail2Ban' do
|
|
82
82
|
end
|
83
83
|
|
84
84
|
it 'IP is not banned' do
|
85
|
-
Rack::Attack::Fail2Ban.banned?('1.2.3.4').must_equal false
|
85
|
+
_(Rack::Attack::Fail2Ban.banned?('1.2.3.4')).must_equal false
|
86
86
|
end
|
87
87
|
end
|
88
88
|
end
|
@@ -98,7 +98,8 @@ describe 'Rack::Attack.Fail2Ban' do
|
|
98
98
|
describe 'making request for other discriminator' do
|
99
99
|
it 'succeeds' do
|
100
100
|
get '/', {}, 'REMOTE_ADDR' => '2.2.3.4'
|
101
|
-
|
101
|
+
|
102
|
+
_(last_response.status).must_equal 200
|
102
103
|
end
|
103
104
|
end
|
104
105
|
|
@@ -108,17 +109,17 @@ describe 'Rack::Attack.Fail2Ban' do
|
|
108
109
|
end
|
109
110
|
|
110
111
|
it 'fails' do
|
111
|
-
last_response.status.must_equal 403
|
112
|
+
_(last_response.status).must_equal 403
|
112
113
|
end
|
113
114
|
|
114
115
|
it 'does not increase fail count' do
|
115
116
|
key = "rack::attack:#{Time.now.to_i / @findtime}:fail2ban:count:1.2.3.4"
|
116
|
-
@cache.store.read(key).must_equal 2
|
117
|
+
_(@cache.store.read(key)).must_equal 2
|
117
118
|
end
|
118
119
|
|
119
120
|
it 'is still banned' do
|
120
121
|
key = "rack::attack:fail2ban:ban:1.2.3.4"
|
121
|
-
@cache.store.read(key).must_equal 1
|
122
|
+
_(@cache.store.read(key)).must_equal 1
|
122
123
|
end
|
123
124
|
end
|
124
125
|
|
@@ -128,17 +129,17 @@ describe 'Rack::Attack.Fail2Ban' do
|
|
128
129
|
end
|
129
130
|
|
130
131
|
it 'fails' do
|
131
|
-
last_response.status.must_equal 403
|
132
|
+
_(last_response.status).must_equal 403
|
132
133
|
end
|
133
134
|
|
134
135
|
it 'does not increase fail count' do
|
135
136
|
key = "rack::attack:#{Time.now.to_i / @findtime}:fail2ban:count:1.2.3.4"
|
136
|
-
@cache.store.read(key).must_equal 2
|
137
|
+
_(@cache.store.read(key)).must_equal 2
|
137
138
|
end
|
138
139
|
|
139
140
|
it 'is still banned' do
|
140
141
|
key = "rack::attack:fail2ban:ban:1.2.3.4"
|
141
|
-
@cache.store.read(key).must_equal 1
|
142
|
+
_(@cache.store.read(key)).must_equal 1
|
142
143
|
end
|
143
144
|
end
|
144
145
|
end
|
@@ -13,7 +13,11 @@ OfflineExamples = Minitest::SharedExamples.new do
|
|
13
13
|
end
|
14
14
|
|
15
15
|
it 'should count' do
|
16
|
-
@cache.
|
16
|
+
@cache.count('cache-test-key', 1)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should delete' do
|
20
|
+
@cache.delete('cache-test-key')
|
17
21
|
end
|
18
22
|
end
|
19
23
|
|
@@ -29,6 +33,18 @@ if defined?(::ActiveSupport::Cache::RedisStore)
|
|
29
33
|
end
|
30
34
|
end
|
31
35
|
|
36
|
+
if defined?(Redis) && defined?(ActiveSupport::Cache::RedisCacheStore) && Redis::VERSION >= '4'
|
37
|
+
describe 'when Redis is offline' do
|
38
|
+
include OfflineExamples
|
39
|
+
|
40
|
+
before do
|
41
|
+
@cache = Rack::Attack::Cache.new
|
42
|
+
# Use presumably unused port for Redis client
|
43
|
+
@cache.store = ActiveSupport::Cache::RedisCacheStore.new(host: '127.0.0.1', port: 3333)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
32
48
|
if defined?(::Dalli)
|
33
49
|
describe 'when Memcached is offline' do
|
34
50
|
include OfflineExamples
|
@@ -45,3 +61,32 @@ if defined?(::Dalli)
|
|
45
61
|
end
|
46
62
|
end
|
47
63
|
end
|
64
|
+
|
65
|
+
if defined?(::Dalli) && defined?(::ActiveSupport::Cache::MemCacheStore)
|
66
|
+
describe 'when Memcached is offline' do
|
67
|
+
include OfflineExamples
|
68
|
+
|
69
|
+
before do
|
70
|
+
Dalli.logger.level = Logger::FATAL
|
71
|
+
|
72
|
+
@cache = Rack::Attack::Cache.new
|
73
|
+
@cache.store = ActiveSupport::Cache::MemCacheStore.new('127.0.0.1:22122')
|
74
|
+
end
|
75
|
+
|
76
|
+
after do
|
77
|
+
Dalli.logger.level = Logger::INFO
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
if defined?(Redis)
|
83
|
+
describe 'when Redis is offline' do
|
84
|
+
include OfflineExamples
|
85
|
+
|
86
|
+
before do
|
87
|
+
@cache = Rack::Attack::Cache.new
|
88
|
+
# Use presumably unused port for Redis client
|
89
|
+
@cache.store = Redis.new(host: '127.0.0.1', port: 3333)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -6,14 +6,14 @@ describe Rack::Attack::PathNormalizer do
|
|
6
6
|
subject { Rack::Attack::PathNormalizer }
|
7
7
|
|
8
8
|
it 'should have a normalize_path method' do
|
9
|
-
subject.normalize_path('/foo').must_equal '/foo'
|
9
|
+
_(subject.normalize_path('/foo')).must_equal '/foo'
|
10
10
|
end
|
11
11
|
|
12
12
|
describe 'FallbackNormalizer' do
|
13
13
|
subject { Rack::Attack::FallbackPathNormalizer }
|
14
14
|
|
15
15
|
it '#normalize_path does not change the path' do
|
16
|
-
subject.normalize_path('').must_equal ''
|
16
|
+
_(subject.normalize_path('')).must_equal ''
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
data/spec/rack_attack_spec.rb
CHANGED
@@ -12,7 +12,7 @@ describe 'Rack::Attack' do
|
|
12
12
|
|
13
13
|
it 'blocks requests with trailing slash' do
|
14
14
|
get '/foo/'
|
15
|
-
last_response.status.must_equal 403
|
15
|
+
_(last_response.status).must_equal 403
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
@@ -22,21 +22,21 @@ describe 'Rack::Attack' do
|
|
22
22
|
Rack::Attack.blocklist("ip #{@bad_ip}") { |req| req.ip == @bad_ip }
|
23
23
|
end
|
24
24
|
|
25
|
-
it
|
26
|
-
Rack::Attack.blocklists.key?("ip #{@bad_ip}").must_equal true
|
27
|
-
|
25
|
+
it 'has a blocklist' do
|
26
|
+
_(Rack::Attack.blocklists.key?("ip #{@bad_ip}")).must_equal true
|
27
|
+
end
|
28
28
|
|
29
29
|
describe "a bad request" do
|
30
30
|
before { get '/', {}, 'REMOTE_ADDR' => @bad_ip }
|
31
31
|
|
32
32
|
it "should return a blocklist response" do
|
33
|
-
last_response.status.must_equal 403
|
34
|
-
last_response.body.must_equal "Forbidden\n"
|
33
|
+
_(last_response.status).must_equal 403
|
34
|
+
_(last_response.body).must_equal "Forbidden\n"
|
35
35
|
end
|
36
36
|
|
37
37
|
it "should tag the env" do
|
38
|
-
last_request.env['rack.attack.matched'].must_equal "ip #{@bad_ip}"
|
39
|
-
last_request.env['rack.attack.match_type'].must_equal :blocklist
|
38
|
+
_(last_request.env['rack.attack.matched']).must_equal "ip #{@bad_ip}"
|
39
|
+
_(last_request.env['rack.attack.match_type']).must_equal :blocklist
|
40
40
|
end
|
41
41
|
|
42
42
|
it_allows_ok_requests
|
@@ -54,25 +54,70 @@ describe 'Rack::Attack' do
|
|
54
54
|
before { get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua }
|
55
55
|
|
56
56
|
it "should allow safelists before blocklists" do
|
57
|
-
last_response.status.must_equal 200
|
57
|
+
_(last_response.status).must_equal 200
|
58
58
|
end
|
59
59
|
|
60
60
|
it "should tag the env" do
|
61
|
-
last_request.env['rack.attack.matched'].must_equal 'good ua'
|
62
|
-
last_request.env['rack.attack.match_type'].must_equal :safelist
|
61
|
+
_(last_request.env['rack.attack.matched']).must_equal 'good ua'
|
62
|
+
_(last_request.env['rack.attack.match_type']).must_equal :safelist
|
63
63
|
end
|
64
64
|
end
|
65
65
|
end
|
66
66
|
|
67
67
|
describe '#blocklisted_response' do
|
68
68
|
it 'should exist' do
|
69
|
-
Rack::Attack.blocklisted_response.must_respond_to :call
|
69
|
+
_(Rack::Attack.blocklisted_response).must_respond_to :call
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
73
73
|
describe '#throttled_response' do
|
74
74
|
it 'should exist' do
|
75
|
-
Rack::Attack.throttled_response.must_respond_to :call
|
75
|
+
_(Rack::Attack.throttled_response).must_respond_to :call
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe 'enabled' do
|
81
|
+
it 'should be enabled by default' do
|
82
|
+
_(Rack::Attack.enabled).must_equal true
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'should directly pass request when disabled' do
|
86
|
+
bad_ip = '1.2.3.4'
|
87
|
+
Rack::Attack.blocklist("ip #{bad_ip}") { |req| req.ip == bad_ip }
|
88
|
+
|
89
|
+
get '/', {}, 'REMOTE_ADDR' => bad_ip
|
90
|
+
_(last_response.status).must_equal 403
|
91
|
+
|
92
|
+
prev_enabled = Rack::Attack.enabled
|
93
|
+
begin
|
94
|
+
Rack::Attack.enabled = false
|
95
|
+
get '/', {}, 'REMOTE_ADDR' => bad_ip
|
96
|
+
_(last_response.status).must_equal 200
|
97
|
+
ensure
|
98
|
+
Rack::Attack.enabled = prev_enabled
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe 'reset!' do
|
104
|
+
it 'raises an error when is not supported by cache store' do
|
105
|
+
Rack::Attack.cache.store = Class.new
|
106
|
+
assert_raises(Rack::Attack::IncompatibleStoreError) do
|
107
|
+
Rack::Attack.reset!
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
if defined?(Redis)
|
112
|
+
it 'should delete rack attack keys' do
|
113
|
+
redis = Redis.new
|
114
|
+
redis.set('key', 'value')
|
115
|
+
redis.set("#{Rack::Attack.cache.prefix}::key", 'value')
|
116
|
+
Rack::Attack.cache.store = redis
|
117
|
+
Rack::Attack.reset!
|
118
|
+
|
119
|
+
_(redis.get('key')).must_equal 'value'
|
120
|
+
_(redis.get("#{Rack::Attack.cache.prefix}::key")).must_be_nil
|
76
121
|
end
|
77
122
|
end
|
78
123
|
end
|
@@ -18,12 +18,19 @@ describe 'Rack::Attack.throttle' do
|
|
18
18
|
|
19
19
|
it 'should set the counter for one request' do
|
20
20
|
key = "rack::attack:#{Time.now.to_i / @period}:ip/sec:1.2.3.4"
|
21
|
-
Rack::Attack.cache.store.read(key).must_equal 1
|
21
|
+
_(Rack::Attack.cache.store.read(key)).must_equal 1
|
22
22
|
end
|
23
23
|
|
24
24
|
it 'should populate throttle data' do
|
25
|
-
data = {
|
26
|
-
|
25
|
+
data = {
|
26
|
+
count: 1,
|
27
|
+
limit: 1,
|
28
|
+
period: @period,
|
29
|
+
epoch_time: Rack::Attack.cache.last_epoch_time.to_i,
|
30
|
+
discriminator: "1.2.3.4"
|
31
|
+
}
|
32
|
+
|
33
|
+
_(last_request.env['rack.attack.throttle_data']['ip/sec']).must_equal data
|
27
34
|
end
|
28
35
|
end
|
29
36
|
|
@@ -33,18 +40,22 @@ describe 'Rack::Attack.throttle' do
|
|
33
40
|
end
|
34
41
|
|
35
42
|
it 'should block the last request' do
|
36
|
-
last_response.status.must_equal 429
|
43
|
+
_(last_response.status).must_equal 429
|
37
44
|
end
|
38
45
|
|
39
46
|
it 'should tag the env' do
|
40
|
-
last_request.env['rack.attack.matched'].must_equal 'ip/sec'
|
41
|
-
last_request.env['rack.attack.match_type'].must_equal :throttle
|
42
|
-
|
43
|
-
last_request.env['rack.attack.
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
47
|
+
_(last_request.env['rack.attack.matched']).must_equal 'ip/sec'
|
48
|
+
_(last_request.env['rack.attack.match_type']).must_equal :throttle
|
49
|
+
|
50
|
+
_(last_request.env['rack.attack.match_data']).must_equal(
|
51
|
+
count: 2,
|
52
|
+
limit: 1,
|
53
|
+
period: @period,
|
54
|
+
epoch_time: Rack::Attack.cache.last_epoch_time.to_i,
|
55
|
+
discriminator: "1.2.3.4"
|
56
|
+
)
|
57
|
+
|
58
|
+
_(last_request.env['rack.attack.match_discriminator']).must_equal('1.2.3.4')
|
48
59
|
end
|
49
60
|
end
|
50
61
|
end
|
@@ -63,12 +74,19 @@ describe 'Rack::Attack.throttle with limit as proc' do
|
|
63
74
|
|
64
75
|
it 'should set the counter for one request' do
|
65
76
|
key = "rack::attack:#{Time.now.to_i / @period}:ip/sec:1.2.3.4"
|
66
|
-
Rack::Attack.cache.store.read(key).must_equal 1
|
77
|
+
_(Rack::Attack.cache.store.read(key)).must_equal 1
|
67
78
|
end
|
68
79
|
|
69
80
|
it 'should populate throttle data' do
|
70
|
-
data = {
|
71
|
-
|
81
|
+
data = {
|
82
|
+
count: 1,
|
83
|
+
limit: 1,
|
84
|
+
period: @period,
|
85
|
+
epoch_time: Rack::Attack.cache.last_epoch_time.to_i,
|
86
|
+
discriminator: "1.2.3.4"
|
87
|
+
}
|
88
|
+
|
89
|
+
_(last_request.env['rack.attack.throttle_data']['ip/sec']).must_equal data
|
72
90
|
end
|
73
91
|
end
|
74
92
|
end
|
@@ -87,12 +105,19 @@ describe 'Rack::Attack.throttle with period as proc' do
|
|
87
105
|
|
88
106
|
it 'should set the counter for one request' do
|
89
107
|
key = "rack::attack:#{Time.now.to_i / @period}:ip/sec:1.2.3.4"
|
90
|
-
Rack::Attack.cache.store.read(key).must_equal 1
|
108
|
+
_(Rack::Attack.cache.store.read(key)).must_equal 1
|
91
109
|
end
|
92
110
|
|
93
111
|
it 'should populate throttle data' do
|
94
|
-
data = {
|
95
|
-
|
112
|
+
data = {
|
113
|
+
count: 1,
|
114
|
+
limit: 1,
|
115
|
+
period: @period,
|
116
|
+
epoch_time: Rack::Attack.cache.last_epoch_time.to_i,
|
117
|
+
discriminator: "1.2.3.4"
|
118
|
+
}
|
119
|
+
|
120
|
+
_(last_request.env['rack.attack.throttle_data']['ip/sec']).must_equal data
|
96
121
|
end
|
97
122
|
end
|
98
123
|
end
|
@@ -25,8 +25,9 @@ describe 'Rack::Attack.track' do
|
|
25
25
|
|
26
26
|
it "should tag the env" do
|
27
27
|
get '/'
|
28
|
-
|
29
|
-
last_request.env['rack.attack.
|
28
|
+
|
29
|
+
_(last_request.env['rack.attack.matched']).must_equal 'everything'
|
30
|
+
_(last_request.env['rack.attack.match_type']).must_equal :track
|
30
31
|
end
|
31
32
|
|
32
33
|
describe "with a notification subscriber and two tracks" do
|
@@ -43,21 +44,23 @@ describe 'Rack::Attack.track' do
|
|
43
44
|
end
|
44
45
|
|
45
46
|
it "should notify twice" do
|
46
|
-
Counter.check.must_equal 2
|
47
|
+
_(Counter.check).must_equal 2
|
47
48
|
end
|
48
49
|
end
|
49
50
|
|
50
51
|
describe "without limit and period options" do
|
51
52
|
it "should assign the track filter to a Check instance" do
|
52
53
|
track = Rack::Attack.track("homepage") { |req| req.path == "/" }
|
53
|
-
|
54
|
+
|
55
|
+
_(track.filter.class).must_equal Rack::Attack::Check
|
54
56
|
end
|
55
57
|
end
|
56
58
|
|
57
59
|
describe "with limit and period options" do
|
58
60
|
it "should assign the track filter to a Throttle instance" do
|
59
61
|
track = Rack::Attack.track("homepage", limit: 10, period: 10) { |req| req.path == "/" }
|
60
|
-
|
62
|
+
|
63
|
+
_(track.filter.class).must_equal Rack::Attack::Throttle
|
61
64
|
end
|
62
65
|
end
|
63
66
|
end
|