rack-attack 5.1.0 → 5.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.
@@ -0,0 +1,48 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "Cache store config when throttling without Rails" do
4
+ before do
5
+ Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
6
+ request.ip
7
+ end
8
+ end
9
+
10
+ it "gives semantic error if no store was configured" do
11
+ assert_raises(Rack::Attack::MissingStoreError) do
12
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
13
+ end
14
+ end
15
+
16
+ it "gives semantic error if incompatible store was configured" do
17
+ Rack::Attack.cache.store = Object.new
18
+
19
+ assert_raises(Rack::Attack::MisconfiguredStoreError) do
20
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
21
+ end
22
+ end
23
+
24
+ it "works with any object that responds to #increment" do
25
+ basic_store_class = Class.new do
26
+ attr_accessor :counts
27
+
28
+ def initialize
29
+ @counts = {}
30
+ end
31
+
32
+ def increment(key, count, options)
33
+ @counts[key] ||= 0
34
+ @counts[key] += 1
35
+ end
36
+ end
37
+
38
+ Rack::Attack.cache.store = basic_store_class.new
39
+
40
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
41
+
42
+ assert_equal 200, last_response.status
43
+
44
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
45
+
46
+ assert_equal 429, last_response.status
47
+ end
48
+ end
@@ -0,0 +1,31 @@
1
+ require_relative "../spec_helper"
2
+ require "minitest/stub_const"
3
+ require "ostruct"
4
+
5
+ describe "Cache store config with Rails" do
6
+ before do
7
+ Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
8
+ request.ip
9
+ end
10
+ end
11
+
12
+ it "fails when Rails.cache is not set" do
13
+ Object.stub_const(:Rails, OpenStruct.new(cache: nil)) do
14
+ assert_raises(Rack::Attack::MissingStoreError) do
15
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
16
+ end
17
+ end
18
+ end
19
+
20
+ it "works when Rails.cache is set" do
21
+ Object.stub_const(:Rails, OpenStruct.new(cache: ActiveSupport::Cache::MemoryStore.new)) do
22
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
23
+
24
+ assert_equal 200, last_response.status
25
+
26
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
27
+
28
+ assert_equal 429, last_response.status
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,41 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "Customizing block responses" 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 "can be customized" do
11
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
12
+
13
+ assert_equal 403, last_response.status
14
+
15
+ Rack::Attack.blocklisted_response = lambda do |env|
16
+ [503, {}, ["Blocked"]]
17
+ end
18
+
19
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
20
+
21
+ assert_equal 503, last_response.status
22
+ assert_equal "Blocked", last_response.body
23
+ end
24
+
25
+ it "exposes match data" do
26
+ matched = nil
27
+ match_type = nil
28
+
29
+ Rack::Attack.blocklisted_response = lambda do |env|
30
+ matched = env['rack.attack.matched']
31
+ match_type = env['rack.attack.match_type']
32
+
33
+ [503, {}, ["Blocked"]]
34
+ end
35
+
36
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
37
+
38
+ assert_equal "block 1.2.3.4", matched
39
+ assert_equal :blocklist, match_type
40
+ end
41
+ end
@@ -0,0 +1,59 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "Customizing throttled response" do
4
+ before do
5
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
6
+
7
+ Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
8
+ request.ip
9
+ end
10
+ end
11
+
12
+ it "can be customized" do
13
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
14
+
15
+ assert_equal 200, last_response.status
16
+
17
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
18
+
19
+ assert_equal 429, last_response.status
20
+
21
+ Rack::Attack.throttled_response = lambda do |env|
22
+ [503, {}, ["Throttled"]]
23
+ end
24
+
25
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
26
+
27
+ assert_equal 503, last_response.status
28
+ assert_equal "Throttled", last_response.body
29
+ end
30
+
31
+ it "exposes match data" do
32
+ matched = nil
33
+ match_type = nil
34
+ match_data = nil
35
+ match_discriminator = nil
36
+
37
+ Rack::Attack.throttled_response = lambda do |env|
38
+ matched = env['rack.attack.matched']
39
+ match_type = env['rack.attack.match_type']
40
+ match_data = env['rack.attack.match_data']
41
+ match_discriminator = env['rack.attack.match_discriminator']
42
+
43
+ [429, {}, ["Throttled"]]
44
+ end
45
+
46
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
47
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
48
+
49
+ assert_equal "by ip", matched
50
+ assert_equal :throttle, match_type
51
+ assert_equal 60, match_data[:period]
52
+ assert_equal 1, match_data[:limit]
53
+ assert_equal 2, match_data[:count]
54
+ assert_equal "1.2.3.4", match_discriminator
55
+
56
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
57
+ assert_equal 3, match_data[:count]
58
+ end
59
+ end
@@ -0,0 +1,34 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "Extending the request object" do
4
+ before do
5
+ class Rack::Attack::Request
6
+ def authorized?
7
+ env["APIKey"] == "private-secret"
8
+ end
9
+ end
10
+
11
+ Rack::Attack.blocklist("unauthorized requests") do |request|
12
+ !request.authorized?
13
+ end
14
+ end
15
+
16
+ # We don't want the extension to leak to other test cases
17
+ after do
18
+ class Rack::Attack::Request
19
+ remove_method :authorized?
20
+ end
21
+ end
22
+
23
+ it "forbids request if blocklist condition is true" do
24
+ get "/"
25
+
26
+ assert_equal 403, last_response.status
27
+ end
28
+
29
+ it "succeeds if blocklist condition is false" do
30
+ get "/", {}, "APIKey" => "private-secret"
31
+
32
+ assert_equal 200, last_response.status
33
+ end
34
+ end
@@ -0,0 +1,76 @@
1
+ require_relative "../spec_helper"
2
+ require "timecop"
3
+
4
+ describe "fail2ban" do
5
+ before do
6
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
7
+
8
+ Rack::Attack.blocklist("fail2ban pentesters") do |request|
9
+ Rack::Attack::Fail2Ban.filter(request.ip, maxretry: 2, findtime: 30, bantime: 60) do
10
+ request.path.include?("private-place")
11
+ end
12
+ end
13
+ end
14
+
15
+ it "returns OK for many requests to non filtered path" do
16
+ get "/"
17
+ assert_equal 200, last_response.status
18
+
19
+ get "/"
20
+ assert_equal 200, last_response.status
21
+ end
22
+
23
+ it "forbids access to private path" do
24
+ get "/private-place"
25
+ assert_equal 403, last_response.status
26
+ end
27
+
28
+ it "returns OK for non filtered path if yet not reached maxretry limit" do
29
+ get "/private-place"
30
+ assert_equal 403, last_response.status
31
+
32
+ get "/"
33
+ assert_equal 200, last_response.status
34
+ end
35
+
36
+ it "forbids all access after reaching maxretry limit" do
37
+ get "/private-place"
38
+ assert_equal 403, last_response.status
39
+
40
+ get "/private-place"
41
+ assert_equal 403, last_response.status
42
+
43
+ get "/"
44
+ assert_equal 403, last_response.status
45
+ end
46
+
47
+ it "restores access after bantime elapsed" do
48
+ get "/private-place"
49
+ assert_equal 403, last_response.status
50
+
51
+ get "/private-place"
52
+ assert_equal 403, last_response.status
53
+
54
+ get "/"
55
+ assert_equal 403, last_response.status
56
+
57
+ Timecop.travel(60) do
58
+ get "/"
59
+
60
+ assert_equal 200, last_response.status
61
+ end
62
+ end
63
+
64
+ it "does not forbid all access if maxrety condition is met but not within the findtime timespan" do
65
+ get "/private-place"
66
+ assert_equal 403, last_response.status
67
+
68
+ Timecop.travel(31) do
69
+ get "/private-place"
70
+ assert_equal 403, last_response.status
71
+
72
+ get "/"
73
+ assert_equal 200, last_response.status
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,49 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "Safelist an IP" do
4
+ before do
5
+ Rack::Attack.blocklist("admin") do |request|
6
+ request.path == "/admin"
7
+ end
8
+
9
+ Rack::Attack.safelist_ip("5.6.7.8")
10
+ end
11
+
12
+ it "forbids request if blocklist condition is true and safelist is false" do
13
+ get "/admin", {}, "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 and safelist is false" do
19
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
20
+
21
+ assert_equal 200, last_response.status
22
+ end
23
+
24
+ it "succeeds request if blocklist condition is false and safelist is true" do
25
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
26
+
27
+ assert_equal 200, last_response.status
28
+ end
29
+
30
+ it "succeeds request if both blocklist and safelist conditions are true" do
31
+ get "/admin", {}, "REMOTE_ADDR" => "5.6.7.8"
32
+
33
+ assert_equal 200, last_response.status
34
+ end
35
+
36
+ it "notifies when the request is safe" do
37
+ notification_type = nil
38
+
39
+ ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
40
+ notification_type = request.env["rack.attack.match_type"]
41
+ end
42
+
43
+ get "/admin", {}, "REMOTE_ADDR" => "5.6.7.8"
44
+
45
+ assert_equal 200, last_response.status
46
+ assert_equal :safelist, notification_type
47
+ end
48
+ end
49
+
@@ -34,4 +34,20 @@ describe "#safelist" do
34
34
 
35
35
  assert_equal 200, last_response.status
36
36
  end
37
+
38
+ it "notifies when the request is safe" do
39
+ notification_matched = nil
40
+ notification_type = nil
41
+
42
+ ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
43
+ notification_matched = request.env["rack.attack.matched"]
44
+ notification_type = request.env["rack.attack.match_type"]
45
+ end
46
+
47
+ get "/safe_space", {}, "REMOTE_ADDR" => "1.2.3.4"
48
+
49
+ assert_equal 200, last_response.status
50
+ assert_equal "safe path", notification_matched
51
+ assert_equal :safelist, notification_type
52
+ end
37
53
  end
@@ -0,0 +1,48 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "Safelisting an IP subnet" do
4
+ before do
5
+ Rack::Attack.blocklist("admin") do |request|
6
+ request.path == "/admin"
7
+ end
8
+
9
+ Rack::Attack.safelist_ip("5.6.0.0/16")
10
+ end
11
+
12
+ it "forbids request if blocklist condition is true and safelist is false" do
13
+ get "/admin", {}, "REMOTE_ADDR" => "5.7.0.0"
14
+
15
+ assert_equal 403, last_response.status
16
+ end
17
+
18
+ it "succeeds if blocklist condition is false and safelist is false" do
19
+ get "/", {}, "REMOTE_ADDR" => "5.7.0.0"
20
+
21
+ assert_equal 200, last_response.status
22
+ end
23
+
24
+ it "succeeds request if blocklist condition is false and safelist is true" do
25
+ get "/", {}, "REMOTE_ADDR" => "5.6.0.0"
26
+
27
+ assert_equal 200, last_response.status
28
+ end
29
+
30
+ it "succeeds request if both blocklist and safelist conditions are true" do
31
+ get "/admin", {}, "REMOTE_ADDR" => "5.6.255.255"
32
+
33
+ assert_equal 200, last_response.status
34
+ end
35
+
36
+ it "notifies when the request is safe" do
37
+ notification_type = nil
38
+
39
+ ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
40
+ notification_type = request.env["rack.attack.match_type"]
41
+ end
42
+
43
+ get "/admin", {}, "REMOTE_ADDR" => "5.6.0.0"
44
+
45
+ assert_equal 200, last_response.status
46
+ assert_equal :safelist, notification_type
47
+ end
48
+ end
@@ -2,9 +2,11 @@ require_relative "../spec_helper"
2
2
  require "timecop"
3
3
 
4
4
  describe "#throttle" do
5
- it "allows one request per minute by IP" do
5
+ before do
6
6
  Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
7
+ end
7
8
 
9
+ it "allows one request per minute by IP" do
8
10
  Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
9
11
  request.ip
10
12
  end
@@ -16,6 +18,8 @@ describe "#throttle" do
16
18
  get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
17
19
 
18
20
  assert_equal 429, last_response.status
21
+ assert_equal "60", last_response.headers["Retry-After"]
22
+ assert_equal "Retry later\n", last_response.body
19
23
 
20
24
  get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
21
25
 
@@ -27,4 +31,129 @@ describe "#throttle" do
27
31
  assert_equal 200, last_response.status
28
32
  end
29
33
  end
34
+
35
+ it "supports limit to be dynamic" do
36
+ # Could be used to have different rate limits for authorized
37
+ # vs general requests
38
+ limit_proc = lambda do |request|
39
+ if request.env["X-APIKey"] == "private-secret"
40
+ 2
41
+ else
42
+ 1
43
+ end
44
+ end
45
+
46
+ Rack::Attack.throttle("by ip", limit: limit_proc, period: 60) do |request|
47
+ request.ip
48
+ end
49
+
50
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
51
+ assert_equal 200, last_response.status
52
+
53
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
54
+ assert_equal 429, last_response.status
55
+
56
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8", "X-APIKey" => "private-secret"
57
+ assert_equal 200, last_response.status
58
+
59
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8", "X-APIKey" => "private-secret"
60
+ assert_equal 200, last_response.status
61
+
62
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8", "X-APIKey" => "private-secret"
63
+ assert_equal 429, last_response.status
64
+ end
65
+
66
+ it "supports period to be dynamic" do
67
+ # Could be used to have different rate limits for authorized
68
+ # vs general requests
69
+ period_proc = lambda do |request|
70
+ if request.env["X-APIKey"] == "private-secret"
71
+ 10
72
+ else
73
+ 30
74
+ end
75
+ end
76
+
77
+ Rack::Attack.throttle("by ip", limit: 1, period: period_proc) do |request|
78
+ request.ip
79
+ end
80
+
81
+ # Using Time#at to align to start/end of periods exactly
82
+ # to achieve consistenty in different test runs
83
+
84
+ Timecop.travel(Time.at(0)) do
85
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
86
+ assert_equal 200, last_response.status
87
+
88
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
89
+ assert_equal 429, last_response.status
90
+ end
91
+
92
+ Timecop.travel(Time.at(10)) do
93
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
94
+ assert_equal 429, last_response.status
95
+ end
96
+
97
+ Timecop.travel(Time.at(30)) do
98
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
99
+ assert_equal 200, last_response.status
100
+ end
101
+
102
+ Timecop.travel(Time.at(0)) do
103
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8", "X-APIKey" => "private-secret"
104
+ assert_equal 200, last_response.status
105
+
106
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8", "X-APIKey" => "private-secret"
107
+ assert_equal 429, last_response.status
108
+ end
109
+
110
+ Timecop.travel(Time.at(10)) do
111
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8", "X-APIKey" => "private-secret"
112
+ assert_equal 200, last_response.status
113
+ end
114
+ end
115
+
116
+ it "notifies when the request is throttled" do
117
+ Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
118
+ request.ip
119
+ end
120
+
121
+ notification_matched = nil
122
+ notification_type = nil
123
+ notification_data = nil
124
+ notification_discriminator = nil
125
+
126
+ ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
127
+ notification_matched = request.env["rack.attack.matched"]
128
+ notification_type = request.env["rack.attack.match_type"]
129
+ notification_data = request.env['rack.attack.match_data']
130
+ notification_discriminator = request.env['rack.attack.match_discriminator']
131
+ end
132
+
133
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
134
+
135
+ assert_equal 200, last_response.status
136
+ assert_nil notification_matched
137
+ assert_nil notification_type
138
+ assert_nil notification_data
139
+ assert_nil notification_discriminator
140
+
141
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
142
+
143
+ assert_equal 200, last_response.status
144
+ assert_nil notification_matched
145
+ assert_nil notification_type
146
+ assert_nil notification_data
147
+ assert_nil notification_discriminator
148
+
149
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
150
+
151
+ assert_equal 429, last_response.status
152
+ assert_equal "by ip", notification_matched
153
+ assert_equal :throttle, notification_type
154
+ assert_equal 60, notification_data[:period]
155
+ assert_equal 1, notification_data[:limit]
156
+ assert_equal 2, notification_data[:count]
157
+ assert_equal "1.2.3.4", notification_discriminator
158
+ end
30
159
  end