rack-attack 5.4.0 → 6.2.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 +4 -4
- data/README.md +78 -27
- data/Rakefile +3 -1
- data/bin/setup +8 -0
- data/lib/rack/attack.rb +137 -148
- data/lib/rack/attack/allow2ban.rb +2 -0
- data/lib/rack/attack/blocklist.rb +3 -1
- data/lib/rack/attack/cache.rb +9 -4
- data/lib/rack/attack/check.rb +5 -2
- data/lib/rack/attack/fail2ban.rb +2 -0
- data/lib/rack/attack/path_normalizer.rb +22 -18
- data/lib/rack/attack/railtie.rb +21 -0
- data/lib/rack/attack/request.rb +2 -0
- data/lib/rack/attack/safelist.rb +3 -1
- data/lib/rack/attack/store_proxy.rb +12 -24
- data/lib/rack/attack/store_proxy/active_support_redis_store_proxy.rb +39 -0
- data/lib/rack/attack/store_proxy/dalli_proxy.rb +27 -13
- data/lib/rack/attack/store_proxy/mem_cache_store_proxy.rb +21 -0
- data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +23 -9
- data/lib/rack/attack/store_proxy/redis_proxy.rb +16 -10
- data/lib/rack/attack/store_proxy/redis_store_proxy.rb +5 -5
- data/lib/rack/attack/throttle.rb +12 -8
- data/lib/rack/attack/track.rb +9 -6
- data/lib/rack/attack/version.rb +3 -1
- data/spec/acceptance/allow2ban_spec.rb +2 -0
- data/spec/acceptance/blocking_ip_spec.rb +4 -2
- data/spec/acceptance/blocking_spec.rb +45 -3
- data/spec/acceptance/blocking_subnet_spec.rb +4 -2
- data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +50 -39
- data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +38 -29
- data/spec/acceptance/cache_store_config_for_throttle_spec.rb +2 -0
- data/spec/acceptance/cache_store_config_with_rails_spec.rb +2 -0
- data/spec/acceptance/customizing_blocked_response_spec.rb +2 -0
- data/spec/acceptance/customizing_throttled_response_spec.rb +2 -0
- data/spec/acceptance/extending_request_object_spec.rb +2 -0
- data/spec/acceptance/fail2ban_spec.rb +2 -0
- data/spec/acceptance/rails_middleware_spec.rb +41 -0
- data/spec/acceptance/safelisting_ip_spec.rb +4 -2
- data/spec/acceptance/safelisting_spec.rb +57 -3
- data/spec/acceptance/safelisting_subnet_spec.rb +4 -2
- data/spec/acceptance/stores/active_support_dalli_store_spec.rb +3 -23
- data/spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb +20 -0
- data/spec/acceptance/stores/active_support_mem_cache_store_spec.rb +4 -24
- data/spec/acceptance/stores/active_support_memory_store_spec.rb +3 -23
- data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +10 -24
- data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +9 -25
- data/spec/acceptance/stores/active_support_redis_store_spec.rb +4 -24
- data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +5 -23
- data/spec/acceptance/stores/dalli_client_spec.rb +3 -23
- data/spec/acceptance/stores/redis_spec.rb +1 -23
- data/spec/acceptance/stores/redis_store_spec.rb +3 -23
- data/spec/acceptance/throttling_spec.rb +7 -5
- data/spec/acceptance/track_spec.rb +5 -3
- data/spec/acceptance/track_throttle_spec.rb +5 -3
- data/spec/allow2ban_spec.rb +20 -15
- data/spec/fail2ban_spec.rb +20 -17
- data/spec/integration/offline_spec.rb +3 -1
- data/spec/rack_attack_dalli_proxy_spec.rb +2 -0
- data/spec/rack_attack_instrumentation_spec.rb +42 -0
- data/spec/rack_attack_path_normalizer_spec.rb +4 -2
- data/spec/rack_attack_request_spec.rb +2 -0
- data/spec/rack_attack_spec.rb +38 -34
- data/spec/rack_attack_throttle_spec.rb +50 -19
- data/spec/rack_attack_track_spec.rb +12 -7
- data/spec/spec_helper.rb +10 -8
- data/spec/support/cache_store_helper.rb +27 -1
- metadata +48 -28
- data/lib/rack/attack/store_proxy/mem_cache_proxy.rb +0 -50
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../spec_helper"
|
4
|
+
|
5
|
+
if defined?(Rails)
|
6
|
+
describe "Middleware for Rails" do
|
7
|
+
before do
|
8
|
+
@app = Class.new(Rails::Application) do
|
9
|
+
config.eager_load = false
|
10
|
+
config.logger = Logger.new(nil) # avoid creating the log/ directory automatically
|
11
|
+
config.cache_store = :null_store # avoid creating tmp/ directory for cache
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
if Gem::Version.new(Rails::VERSION::STRING) >= Gem::Version.new("5.1")
|
16
|
+
it "is used by default" do
|
17
|
+
@app.initialize!
|
18
|
+
assert_equal 1, @app.middleware.count(Rack::Attack)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "is not added when it was added explicitly" do
|
22
|
+
@app.config.middleware.use(Rack::Attack)
|
23
|
+
@app.initialize!
|
24
|
+
assert_equal 1, @app.middleware.count(Rack::Attack)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "is not added when it was explicitly deleted" do
|
28
|
+
@app.config.middleware.delete(Rack::Attack)
|
29
|
+
@app.initialize!
|
30
|
+
refute @app.middleware.include?(Rack::Attack)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
if Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new("5.1")
|
35
|
+
it "is not used by default" do
|
36
|
+
@app.initialize!
|
37
|
+
assert_equal 0, @app.middleware.count(Rack::Attack)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../spec_helper"
|
2
4
|
|
3
5
|
describe "Safelist an IP" do
|
@@ -36,8 +38,8 @@ describe "Safelist an IP" do
|
|
36
38
|
it "notifies when the request is safe" do
|
37
39
|
notification_type = nil
|
38
40
|
|
39
|
-
ActiveSupport::Notifications.subscribe("
|
40
|
-
notification_type = request.env["rack.attack.match_type"]
|
41
|
+
ActiveSupport::Notifications.subscribe("safelist.rack_attack") do |_name, _start, _finish, _id, payload|
|
42
|
+
notification_type = payload[:request].env["rack.attack.match_type"]
|
41
43
|
end
|
42
44
|
|
43
45
|
get "/admin", {}, "REMOTE_ADDR" => "5.6.7.8"
|
@@ -1,6 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../spec_helper"
|
2
4
|
|
3
5
|
describe "#safelist" do
|
6
|
+
before do
|
7
|
+
Rack::Attack.blocklist do |request|
|
8
|
+
request.ip == "1.2.3.4"
|
9
|
+
end
|
10
|
+
|
11
|
+
Rack::Attack.safelist do |request|
|
12
|
+
request.path == "/safe_space"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it "forbids request if blocklist condition is true and safelist is false" do
|
17
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
18
|
+
|
19
|
+
assert_equal 403, last_response.status
|
20
|
+
end
|
21
|
+
|
22
|
+
it "succeeds if blocklist condition is false and safelist is false" do
|
23
|
+
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
24
|
+
|
25
|
+
assert_equal 200, last_response.status
|
26
|
+
end
|
27
|
+
|
28
|
+
it "succeeds request if blocklist condition is false and safelist is true" do
|
29
|
+
get "/safe_space", {}, "REMOTE_ADDR" => "5.6.7.8"
|
30
|
+
|
31
|
+
assert_equal 200, last_response.status
|
32
|
+
end
|
33
|
+
|
34
|
+
it "succeeds request if both blocklist and safelist conditions are true" do
|
35
|
+
get "/safe_space", {}, "REMOTE_ADDR" => "1.2.3.4"
|
36
|
+
|
37
|
+
assert_equal 200, last_response.status
|
38
|
+
end
|
39
|
+
|
40
|
+
it "notifies when the request is safe" do
|
41
|
+
notification_matched = nil
|
42
|
+
notification_type = nil
|
43
|
+
|
44
|
+
ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, payload|
|
45
|
+
notification_matched = payload[:request].env["rack.attack.matched"]
|
46
|
+
notification_type = payload[:request].env["rack.attack.match_type"]
|
47
|
+
end
|
48
|
+
|
49
|
+
get "/safe_space", {}, "REMOTE_ADDR" => "1.2.3.4"
|
50
|
+
|
51
|
+
assert_equal 200, last_response.status
|
52
|
+
assert_nil notification_matched
|
53
|
+
assert_equal :safelist, notification_type
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#safelist with name" do
|
4
58
|
before do
|
5
59
|
Rack::Attack.blocklist("block 1.2.3.4") do |request|
|
6
60
|
request.ip == "1.2.3.4"
|
@@ -39,9 +93,9 @@ describe "#safelist" do
|
|
39
93
|
notification_matched = nil
|
40
94
|
notification_type = nil
|
41
95
|
|
42
|
-
ActiveSupport::Notifications.subscribe("
|
43
|
-
notification_matched = request.env["rack.attack.matched"]
|
44
|
-
notification_type = request.env["rack.attack.match_type"]
|
96
|
+
ActiveSupport::Notifications.subscribe("safelist.rack_attack") do |_name, _start, _finish, _id, payload|
|
97
|
+
notification_matched = payload[:request].env["rack.attack.matched"]
|
98
|
+
notification_type = payload[:request].env["rack.attack.match_type"]
|
45
99
|
end
|
46
100
|
|
47
101
|
get "/safe_space", {}, "REMOTE_ADDR" => "1.2.3.4"
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../spec_helper"
|
2
4
|
|
3
5
|
describe "Safelisting an IP subnet" do
|
@@ -36,8 +38,8 @@ describe "Safelisting an IP subnet" do
|
|
36
38
|
it "notifies when the request is safe" do
|
37
39
|
notification_type = nil
|
38
40
|
|
39
|
-
ActiveSupport::Notifications.subscribe("
|
40
|
-
notification_type = request.env["rack.attack.match_type"]
|
41
|
+
ActiveSupport::Notifications.subscribe("safelist.rack_attack") do |_name, _start, _finish, _id, payload|
|
42
|
+
notification_type = payload[:request].env["rack.attack.match_type"]
|
41
43
|
end
|
42
44
|
|
43
45
|
get "/admin", {}, "REMOTE_ADDR" => "5.6.0.0"
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../../spec_helper"
|
2
4
|
|
3
5
|
if defined?(::Dalli)
|
@@ -14,28 +16,6 @@ if defined?(::Dalli)
|
|
14
16
|
Rack::Attack.cache.store.clear
|
15
17
|
end
|
16
18
|
|
17
|
-
it_works_for_cache_backed_features
|
18
|
-
|
19
|
-
it "doesn't leak keys" do
|
20
|
-
Rack::Attack.throttle("by ip", limit: 1, period: 1) do |request|
|
21
|
-
request.ip
|
22
|
-
end
|
23
|
-
|
24
|
-
key = nil
|
25
|
-
|
26
|
-
# Freeze time during these statement to be sure that the key used by rack attack is the same
|
27
|
-
# we pre-calculate in local variable `key`
|
28
|
-
Timecop.freeze do
|
29
|
-
key = "rack::attack:#{Time.now.to_i}:by ip:1.2.3.4"
|
30
|
-
|
31
|
-
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
32
|
-
end
|
33
|
-
|
34
|
-
assert Rack::Attack.cache.store.fetch(key)
|
35
|
-
|
36
|
-
sleep 2.1
|
37
|
-
|
38
|
-
assert_nil Rack::Attack.cache.store.fetch(key)
|
39
|
-
end
|
19
|
+
it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.fetch(key) })
|
40
20
|
end
|
41
21
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../spec_helper"
|
4
|
+
|
5
|
+
if defined?(::ConnectionPool) && defined?(::Dalli)
|
6
|
+
require_relative "../../support/cache_store_helper"
|
7
|
+
require "timecop"
|
8
|
+
|
9
|
+
describe "ActiveSupport::Cache::MemCacheStore (pooled) as a cache backend" do
|
10
|
+
before do
|
11
|
+
Rack::Attack.cache.store = ActiveSupport::Cache::MemCacheStore.new(pool_size: 2)
|
12
|
+
end
|
13
|
+
|
14
|
+
after do
|
15
|
+
Rack::Attack.cache.store.clear
|
16
|
+
end
|
17
|
+
|
18
|
+
it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.read(key) })
|
19
|
+
end
|
20
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../../spec_helper"
|
2
4
|
|
3
5
|
if defined?(::Dalli)
|
@@ -10,31 +12,9 @@ if defined?(::Dalli)
|
|
10
12
|
end
|
11
13
|
|
12
14
|
after do
|
13
|
-
Rack::Attack.cache.store.
|
15
|
+
Rack::Attack.cache.store.clear
|
14
16
|
end
|
15
17
|
|
16
|
-
it_works_for_cache_backed_features
|
17
|
-
|
18
|
-
it "doesn't leak keys" do
|
19
|
-
Rack::Attack.throttle("by ip", limit: 1, period: 1) do |request|
|
20
|
-
request.ip
|
21
|
-
end
|
22
|
-
|
23
|
-
key = nil
|
24
|
-
|
25
|
-
# Freeze time during these statement to be sure that the key used by rack attack is the same
|
26
|
-
# we pre-calculate in local variable `key`
|
27
|
-
Timecop.freeze do
|
28
|
-
key = "rack::attack:#{Time.now.to_i}:by ip:1.2.3.4"
|
29
|
-
|
30
|
-
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
31
|
-
end
|
32
|
-
|
33
|
-
assert Rack::Attack.cache.store.get(key)
|
34
|
-
|
35
|
-
sleep 2.1
|
36
|
-
|
37
|
-
assert_nil Rack::Attack.cache.store.get(key)
|
38
|
-
end
|
18
|
+
it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.read(key) })
|
39
19
|
end
|
40
20
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../../spec_helper"
|
2
4
|
require_relative "../../support/cache_store_helper"
|
3
5
|
|
@@ -12,27 +14,5 @@ describe "ActiveSupport::Cache::MemoryStore as a cache backend" do
|
|
12
14
|
Rack::Attack.cache.store.clear
|
13
15
|
end
|
14
16
|
|
15
|
-
it_works_for_cache_backed_features
|
16
|
-
|
17
|
-
it "doesn't leak keys" do
|
18
|
-
Rack::Attack.throttle("by ip", limit: 1, period: 1) do |request|
|
19
|
-
request.ip
|
20
|
-
end
|
21
|
-
|
22
|
-
key = nil
|
23
|
-
|
24
|
-
# Freeze time during these statement to be sure that the key used by rack attack is the same
|
25
|
-
# we pre-calculate in local variable `key`
|
26
|
-
Timecop.freeze do
|
27
|
-
key = "rack::attack:#{Time.now.to_i}:by ip:1.2.3.4"
|
28
|
-
|
29
|
-
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
30
|
-
end
|
31
|
-
|
32
|
-
assert Rack::Attack.cache.store.fetch(key)
|
33
|
-
|
34
|
-
sleep 2.1
|
35
|
-
|
36
|
-
assert_nil Rack::Attack.cache.store.fetch(key)
|
37
|
-
end
|
17
|
+
it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.fetch(key) })
|
38
18
|
end
|
@@ -1,6 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../../spec_helper"
|
2
4
|
|
3
|
-
|
5
|
+
should_run =
|
6
|
+
defined?(::ConnectionPool) &&
|
7
|
+
defined?(::Redis) &&
|
8
|
+
Gem::Version.new(::Redis::VERSION) >= Gem::Version.new("4") &&
|
9
|
+
defined?(::ActiveSupport::Cache::RedisCacheStore)
|
10
|
+
|
11
|
+
if should_run
|
4
12
|
require_relative "../../support/cache_store_helper"
|
5
13
|
require "timecop"
|
6
14
|
|
@@ -13,28 +21,6 @@ if defined?(::ConnectionPool) && defined?(::Redis) && defined?(::ActiveSupport::
|
|
13
21
|
Rack::Attack.cache.store.clear
|
14
22
|
end
|
15
23
|
|
16
|
-
it_works_for_cache_backed_features
|
17
|
-
|
18
|
-
it "doesn't leak keys" do
|
19
|
-
Rack::Attack.throttle("by ip", limit: 1, period: 1) do |request|
|
20
|
-
request.ip
|
21
|
-
end
|
22
|
-
|
23
|
-
key = nil
|
24
|
-
|
25
|
-
# Freeze time during these statement to be sure that the key used by rack attack is the same
|
26
|
-
# we pre-calculate in local variable `key`
|
27
|
-
Timecop.freeze do
|
28
|
-
key = "rack::attack:#{Time.now.to_i}:by ip:1.2.3.4"
|
29
|
-
|
30
|
-
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
31
|
-
end
|
32
|
-
|
33
|
-
assert Rack::Attack.cache.store.fetch(key)
|
34
|
-
|
35
|
-
sleep 2.1
|
36
|
-
|
37
|
-
assert_nil Rack::Attack.cache.store.fetch(key)
|
38
|
-
end
|
24
|
+
it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.fetch(key) })
|
39
25
|
end
|
40
26
|
end
|
@@ -1,6 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../../spec_helper"
|
2
4
|
|
3
|
-
|
5
|
+
should_run =
|
6
|
+
defined?(::Redis) &&
|
7
|
+
Gem::Version.new(::Redis::VERSION) >= Gem::Version.new("4") &&
|
8
|
+
defined?(::ActiveSupport::Cache::RedisCacheStore)
|
9
|
+
|
10
|
+
if should_run
|
4
11
|
require_relative "../../support/cache_store_helper"
|
5
12
|
require "timecop"
|
6
13
|
|
@@ -13,29 +20,6 @@ if defined?(::Redis) && defined?(::ActiveSupport::Cache::RedisCacheStore)
|
|
13
20
|
Rack::Attack.cache.store.clear
|
14
21
|
end
|
15
22
|
|
16
|
-
it_works_for_cache_backed_features
|
17
|
-
|
18
|
-
it "doesn't leak keys" do
|
19
|
-
Rack::Attack.throttle("by ip", limit: 1, period: 1) do |request|
|
20
|
-
request.ip
|
21
|
-
end
|
22
|
-
|
23
|
-
key = nil
|
24
|
-
|
25
|
-
# Freeze time during these statement to be sure that the key used by rack attack is the same
|
26
|
-
# we pre-calculate in local variable `key`
|
27
|
-
Timecop.freeze do
|
28
|
-
key = "rack::attack:#{Time.now.to_i}:by ip:1.2.3.4"
|
29
|
-
|
30
|
-
# puts key
|
31
|
-
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
32
|
-
end
|
33
|
-
|
34
|
-
assert Rack::Attack.cache.store.fetch(key)
|
35
|
-
|
36
|
-
sleep 2.1
|
37
|
-
|
38
|
-
assert_nil Rack::Attack.cache.store.fetch(key)
|
39
|
-
end
|
23
|
+
it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.fetch(key) })
|
40
24
|
end
|
41
25
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../../spec_helper"
|
2
4
|
|
3
5
|
if defined?(::ActiveSupport::Cache::RedisStore)
|
@@ -10,31 +12,9 @@ if defined?(::ActiveSupport::Cache::RedisStore)
|
|
10
12
|
end
|
11
13
|
|
12
14
|
after do
|
13
|
-
Rack::Attack.cache.store.
|
15
|
+
Rack::Attack.cache.store.clear
|
14
16
|
end
|
15
17
|
|
16
|
-
it_works_for_cache_backed_features
|
17
|
-
|
18
|
-
it "doesn't leak keys" do
|
19
|
-
Rack::Attack.throttle("by ip", limit: 1, period: 1) do |request|
|
20
|
-
request.ip
|
21
|
-
end
|
22
|
-
|
23
|
-
key = nil
|
24
|
-
|
25
|
-
# Freeze time during these statement to be sure that the key used by rack attack is the same
|
26
|
-
# we pre-calculate in local variable `key`
|
27
|
-
Timecop.freeze do
|
28
|
-
key = "rack::attack:#{Time.now.to_i}:by ip:1.2.3.4"
|
29
|
-
|
30
|
-
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
31
|
-
end
|
32
|
-
|
33
|
-
assert Rack::Attack.cache.store.read(key)
|
34
|
-
|
35
|
-
sleep 2.1
|
36
|
-
|
37
|
-
assert_nil Rack::Attack.cache.store.read(key)
|
38
|
-
end
|
18
|
+
it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.read(key) })
|
39
19
|
end
|
40
20
|
end
|