rack-attack 5.4.2 → 6.2.2
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/lib/rack/attack.rb +138 -149
- 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 +13 -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 -14
- 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 +3 -1
- data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +22 -8
- data/lib/rack/attack/store_proxy/redis_proxy.rb +16 -14
- 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 +8 -12
- data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +8 -12
- 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 +35 -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 +2 -0
- data/spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb +1 -3
- data/spec/acceptance/stores/active_support_mem_cache_store_spec.rb +2 -0
- data/spec/acceptance/stores/active_support_memory_store_spec.rb +2 -0
- data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +9 -1
- data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +8 -1
- data/spec/acceptance/stores/active_support_redis_store_spec.rb +3 -1
- data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +5 -3
- data/spec/acceptance/stores/dalli_client_spec.rb +2 -0
- data/spec/acceptance/stores/redis_store_spec.rb +2 -0
- 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 +15 -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 +12 -8
- data/spec/support/cache_store_helper.rb +2 -0
- metadata +71 -56
- data/bin/setup +0 -8
- data/lib/rack/attack/store_proxy/mem_cache_proxy.rb +0 -50
data/lib/rack/attack/throttle.rb
CHANGED
@@ -1,15 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rack
|
2
4
|
class Attack
|
3
5
|
class Throttle
|
4
6
|
MANDATORY_OPTIONS = [:limit, :period].freeze
|
5
7
|
|
6
8
|
attr_reader :name, :limit, :period, :block, :type
|
7
|
-
def initialize(name, options, block)
|
8
|
-
@name
|
9
|
+
def initialize(name, options, &block)
|
10
|
+
@name = name
|
11
|
+
@block = block
|
9
12
|
MANDATORY_OPTIONS.each do |opt|
|
10
|
-
raise ArgumentError
|
13
|
+
raise ArgumentError, "Must pass #{opt.inspect} option" unless options[opt]
|
11
14
|
end
|
12
|
-
@limit
|
15
|
+
@limit = options[:limit]
|
13
16
|
@period = options[:period].respond_to?(:call) ? options[:period] : options[:period].to_i
|
14
17
|
@type = options.fetch(:type, :throttle)
|
15
18
|
end
|
@@ -29,10 +32,11 @@ module Rack
|
|
29
32
|
epoch_time = cache.last_epoch_time
|
30
33
|
|
31
34
|
data = {
|
32
|
-
:
|
33
|
-
:
|
34
|
-
:
|
35
|
-
:
|
35
|
+
discriminator: discriminator,
|
36
|
+
count: count,
|
37
|
+
period: current_period,
|
38
|
+
limit: current_limit,
|
39
|
+
epoch_time: epoch_time
|
36
40
|
}
|
37
41
|
|
38
42
|
(request.env['rack.attack.throttle_data'] ||= {})[name] = data
|
data/lib/rack/attack/track.rb
CHANGED
@@ -1,16 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rack
|
2
4
|
class Attack
|
3
5
|
class Track
|
4
6
|
attr_reader :filter
|
5
7
|
|
6
|
-
def initialize(name, options = {}, block)
|
8
|
+
def initialize(name, options = {}, &block)
|
7
9
|
options[:type] = :track
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
@filter =
|
12
|
+
if options[:limit] && options[:period]
|
13
|
+
Throttle.new(name, options, &block)
|
14
|
+
else
|
15
|
+
Check.new(name, options, &block)
|
16
|
+
end
|
14
17
|
end
|
15
18
|
|
16
19
|
def matched_by?(request)
|
data/lib/rack/attack/version.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../spec_helper"
|
2
4
|
|
3
5
|
describe "Blocking an IP" do
|
@@ -21,9 +23,9 @@ describe "Blocking an IP" do
|
|
21
23
|
notified = false
|
22
24
|
notification_type = nil
|
23
25
|
|
24
|
-
ActiveSupport::Notifications.subscribe("
|
26
|
+
ActiveSupport::Notifications.subscribe("blocklist.rack_attack") do |_name, _start, _finish, _id, payload|
|
25
27
|
notified = true
|
26
|
-
notification_type = request.env["rack.attack.match_type"]
|
28
|
+
notification_type = payload[:request].env["rack.attack.match_type"]
|
27
29
|
end
|
28
30
|
|
29
31
|
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
@@ -1,6 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../spec_helper"
|
2
4
|
|
3
5
|
describe "#blocklist" do
|
6
|
+
before do
|
7
|
+
Rack::Attack.blocklist do |request|
|
8
|
+
request.ip == "1.2.3.4"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
it "forbids request if blocklist condition is true" do
|
13
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
14
|
+
|
15
|
+
assert_equal 403, last_response.status
|
16
|
+
end
|
17
|
+
|
18
|
+
it "succeeds if blocklist condition is false" do
|
19
|
+
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
20
|
+
|
21
|
+
assert_equal 200, last_response.status
|
22
|
+
end
|
23
|
+
|
24
|
+
it "notifies when the request is blocked" do
|
25
|
+
notification_matched = nil
|
26
|
+
notification_type = nil
|
27
|
+
|
28
|
+
ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, payload|
|
29
|
+
notification_matched = payload[:request].env["rack.attack.matched"]
|
30
|
+
notification_type = payload[:request].env["rack.attack.match_type"]
|
31
|
+
end
|
32
|
+
|
33
|
+
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
34
|
+
|
35
|
+
assert_nil notification_matched
|
36
|
+
assert_nil notification_type
|
37
|
+
|
38
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
39
|
+
|
40
|
+
assert_nil notification_matched
|
41
|
+
assert_equal :blocklist, notification_type
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#blocklist with name" do
|
4
46
|
before do
|
5
47
|
Rack::Attack.blocklist("block 1.2.3.4") do |request|
|
6
48
|
request.ip == "1.2.3.4"
|
@@ -23,9 +65,9 @@ describe "#blocklist" do
|
|
23
65
|
notification_matched = nil
|
24
66
|
notification_type = nil
|
25
67
|
|
26
|
-
ActiveSupport::Notifications.subscribe("
|
27
|
-
notification_matched = request.env["rack.attack.matched"]
|
28
|
-
notification_type = request.env["rack.attack.match_type"]
|
68
|
+
ActiveSupport::Notifications.subscribe("blocklist.rack_attack") do |_name, _start, _finish, _id, payload|
|
69
|
+
notification_matched = payload[:request].env["rack.attack.matched"]
|
70
|
+
notification_type = payload[:request].env["rack.attack.match_type"]
|
29
71
|
end
|
30
72
|
|
31
73
|
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../spec_helper"
|
2
4
|
|
3
5
|
describe "Blocking an IP subnet" do
|
@@ -27,9 +29,9 @@ describe "Blocking an IP subnet" do
|
|
27
29
|
notified = false
|
28
30
|
notification_type = nil
|
29
31
|
|
30
|
-
ActiveSupport::Notifications.subscribe("
|
32
|
+
ActiveSupport::Notifications.subscribe("blocklist.rack_attack") do |_name, _start, _finish, _id, payload|
|
31
33
|
notified = true
|
32
|
-
notification_type = request.env["rack.attack.match_type"]
|
34
|
+
notification_type = payload[:request].env["rack.attack.match_type"]
|
33
35
|
end
|
34
36
|
|
35
37
|
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../spec_helper"
|
2
4
|
require "minitest/stub_const"
|
3
5
|
|
@@ -20,11 +22,9 @@ describe "Cache store config when using allow2ban" do
|
|
20
22
|
raised_exception = nil
|
21
23
|
|
22
24
|
fake_store_class = Class.new do
|
23
|
-
def write(key, value)
|
24
|
-
end
|
25
|
+
def write(key, value); end
|
25
26
|
|
26
|
-
def increment(key, count, options = {})
|
27
|
-
end
|
27
|
+
def increment(key, count, options = {}); end
|
28
28
|
end
|
29
29
|
|
30
30
|
Object.stub_const(:FakeStore, fake_store_class) do
|
@@ -42,11 +42,9 @@ describe "Cache store config when using allow2ban" do
|
|
42
42
|
raised_exception = nil
|
43
43
|
|
44
44
|
fake_store_class = Class.new do
|
45
|
-
def read(key)
|
46
|
-
end
|
45
|
+
def read(key); end
|
47
46
|
|
48
|
-
def increment(key, count, options = {})
|
49
|
-
end
|
47
|
+
def increment(key, count, options = {}); end
|
50
48
|
end
|
51
49
|
|
52
50
|
Object.stub_const(:FakeStore, fake_store_class) do
|
@@ -64,11 +62,9 @@ describe "Cache store config when using allow2ban" do
|
|
64
62
|
raised_exception = nil
|
65
63
|
|
66
64
|
fake_store_class = Class.new do
|
67
|
-
def read(key)
|
68
|
-
end
|
65
|
+
def read(key); end
|
69
66
|
|
70
|
-
def write(key, value)
|
71
|
-
end
|
67
|
+
def write(key, value); end
|
72
68
|
end
|
73
69
|
|
74
70
|
Object.stub_const(:FakeStore, fake_store_class) do
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../spec_helper"
|
2
4
|
require "minitest/stub_const"
|
3
5
|
|
@@ -20,11 +22,9 @@ describe "Cache store config when using fail2ban" do
|
|
20
22
|
raised_exception = nil
|
21
23
|
|
22
24
|
fake_store_class = Class.new do
|
23
|
-
def write(key, value)
|
24
|
-
end
|
25
|
+
def write(key, value); end
|
25
26
|
|
26
|
-
def increment(key, count, options = {})
|
27
|
-
end
|
27
|
+
def increment(key, count, options = {}); end
|
28
28
|
end
|
29
29
|
|
30
30
|
Object.stub_const(:FakeStore, fake_store_class) do
|
@@ -42,11 +42,9 @@ describe "Cache store config when using fail2ban" do
|
|
42
42
|
raised_exception = nil
|
43
43
|
|
44
44
|
fake_store_class = Class.new do
|
45
|
-
def read(key)
|
46
|
-
end
|
45
|
+
def read(key); end
|
47
46
|
|
48
|
-
def increment(key, count, options = {})
|
49
|
-
end
|
47
|
+
def increment(key, count, options = {}); end
|
50
48
|
end
|
51
49
|
|
52
50
|
Object.stub_const(:FakeStore, fake_store_class) do
|
@@ -64,11 +62,9 @@ describe "Cache store config when using fail2ban" do
|
|
64
62
|
raised_exception = nil
|
65
63
|
|
66
64
|
fake_store_class = Class.new do
|
67
|
-
def read(key)
|
68
|
-
end
|
65
|
+
def read(key); end
|
69
66
|
|
70
|
-
def write(key, value)
|
71
|
-
end
|
67
|
+
def write(key, value); end
|
72
68
|
end
|
73
69
|
|
74
70
|
Object.stub_const(:FakeStore, fake_store_class) do
|
@@ -0,0 +1,35 @@
|
|
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 explicitly deleted" do
|
22
|
+
@app.config.middleware.delete(Rack::Attack)
|
23
|
+
@app.initialize!
|
24
|
+
refute @app.middleware.include?(Rack::Attack)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
if Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new("5.1")
|
29
|
+
it "is not used by default" do
|
30
|
+
@app.initialize!
|
31
|
+
assert_equal 0, @app.middleware.count(Rack::Attack)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
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"
|