rack-attack 5.0.1 → 5.4.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 +5 -5
- data/README.md +190 -94
- data/Rakefile +11 -4
- data/bin/setup +8 -0
- data/lib/rack/attack.rb +83 -51
- data/lib/rack/attack/allow2ban.rb +2 -1
- data/lib/rack/attack/blocklist.rb +0 -1
- data/lib/rack/attack/cache.rb +24 -5
- data/lib/rack/attack/check.rb +6 -8
- data/lib/rack/attack/fail2ban.rb +2 -1
- data/lib/rack/attack/path_normalizer.rb +6 -11
- data/lib/rack/attack/safelist.rb +0 -1
- data/lib/rack/attack/store_proxy.rb +3 -12
- data/lib/rack/attack/store_proxy/dalli_proxy.rb +2 -3
- data/lib/rack/attack/store_proxy/mem_cache_proxy.rb +4 -5
- data/lib/rack/attack/store_proxy/mem_cache_store_proxy.rb +19 -0
- data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +35 -0
- data/lib/rack/attack/store_proxy/redis_proxy.rb +54 -0
- data/lib/rack/attack/store_proxy/redis_store_proxy.rb +5 -24
- data/lib/rack/attack/throttle.rb +16 -12
- data/lib/rack/attack/track.rb +3 -3
- data/lib/rack/attack/version.rb +1 -1
- data/spec/acceptance/allow2ban_spec.rb +71 -0
- data/spec/acceptance/blocking_ip_spec.rb +38 -0
- data/spec/acceptance/blocking_spec.rb +41 -0
- data/spec/acceptance/blocking_subnet_spec.rb +44 -0
- data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +126 -0
- data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +121 -0
- data/spec/acceptance/cache_store_config_for_throttle_spec.rb +48 -0
- data/spec/acceptance/cache_store_config_with_rails_spec.rb +31 -0
- data/spec/acceptance/customizing_blocked_response_spec.rb +41 -0
- data/spec/acceptance/customizing_throttled_response_spec.rb +59 -0
- data/spec/acceptance/extending_request_object_spec.rb +34 -0
- data/spec/acceptance/fail2ban_spec.rb +76 -0
- data/spec/acceptance/safelisting_ip_spec.rb +48 -0
- data/spec/acceptance/safelisting_spec.rb +53 -0
- data/spec/acceptance/safelisting_subnet_spec.rb +48 -0
- data/spec/acceptance/stores/active_support_dalli_store_spec.rb +19 -0
- data/spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb +22 -0
- data/spec/acceptance/stores/active_support_mem_cache_store_spec.rb +18 -0
- data/spec/acceptance/stores/active_support_memory_store_spec.rb +16 -0
- data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +18 -0
- data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +18 -0
- data/spec/acceptance/stores/active_support_redis_store_spec.rb +18 -0
- data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +22 -0
- data/spec/acceptance/stores/dalli_client_spec.rb +19 -0
- data/spec/acceptance/stores/redis_spec.rb +20 -0
- data/spec/acceptance/stores/redis_store_spec.rb +18 -0
- data/spec/acceptance/throttling_spec.rb +159 -0
- data/spec/acceptance/track_spec.rb +27 -0
- data/spec/acceptance/track_throttle_spec.rb +53 -0
- data/spec/allow2ban_spec.rb +9 -8
- data/spec/fail2ban_spec.rb +11 -9
- data/spec/integration/offline_spec.rb +21 -23
- data/spec/rack_attack_dalli_proxy_spec.rb +0 -2
- data/spec/rack_attack_request_spec.rb +1 -1
- data/spec/rack_attack_spec.rb +13 -14
- data/spec/rack_attack_throttle_spec.rb +28 -18
- data/spec/rack_attack_track_spec.rb +11 -8
- data/spec/spec_helper.rb +35 -14
- data/spec/support/cache_store_helper.rb +82 -0
- metadata +150 -65
- data/spec/integration/rack_attack_cache_spec.rb +0 -122
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e666812691cc414692f7125979f0b152a9111ccee075e65b811fa4a6d8770daa
|
|
4
|
+
data.tar.gz: 3e8caba79f7ad09d4999cce6358de9cc29b815dee3c9f9c5adbf12763c764656
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f630c0cd1a34bd588e616653a2e6795e2ec6baafc0e0df8b489e6aa451cf47fb64065447fb3ceb2b029a51d87a0393d6d44cea02e58423626ea46165531f7da3
|
|
7
|
+
data.tar.gz: 22efc414db06b0a1bbbf8e6d34a3e0d0ead64f1832f48dc30af7ec0a374ef215d53cacf0a8e95c842bff0af84df0939f27820e4668c739eb8db3c51ebba4088e
|
data/README.md
CHANGED
|
@@ -1,105 +1,106 @@
|
|
|
1
|
-
# Rack::Attack
|
|
2
|
-
*Rack middleware for blocking & throttling abusive requests*
|
|
3
|
-
|
|
4
|
-
Rack::Attack is a rack middleware to protect your web app from bad clients.
|
|
5
|
-
It allows *safelisting*, *blocklisting*, *throttling*, and *tracking* based on arbitrary properties of the request.
|
|
1
|
+
# Rack::Attack
|
|
6
2
|
|
|
7
|
-
|
|
3
|
+
*Rack middleware for blocking & throttling abusive requests*
|
|
8
4
|
|
|
9
|
-
|
|
5
|
+
Protect your Rails and Rack apps from bad clients. Rack::Attack lets you easily decide when to *allow*, *block* and *throttle* based on properties of the request.
|
|
10
6
|
|
|
11
|
-
[
|
|
12
|
-
[](https://travis-ci.org/kickstarter/rack-attack)
|
|
13
|
-
[](https://codeclimate.com/github/kickstarter/rack-attack)
|
|
7
|
+
See the [Backing & Hacking blog post](https://www.kickstarter.com/backing-and-hacking/rack-attack-protection-from-abusive-clients) introducing Rack::Attack.
|
|
14
8
|
|
|
9
|
+
[](https://badge.fury.io/rb/rack-attack)
|
|
10
|
+
[](https://travis-ci.org/kickstarter/rack-attack)
|
|
11
|
+
[](https://codeclimate.com/github/kickstarter/rack-attack)
|
|
15
12
|
|
|
16
13
|
## Getting started
|
|
17
14
|
|
|
18
|
-
|
|
15
|
+
### 1. Installing
|
|
16
|
+
|
|
17
|
+
Add this line to your application's Gemfile:
|
|
19
18
|
|
|
20
19
|
```ruby
|
|
21
20
|
# In your Gemfile
|
|
21
|
+
|
|
22
22
|
gem 'rack-attack'
|
|
23
23
|
```
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
|
|
25
|
+
And then execute:
|
|
26
|
+
|
|
27
|
+
$ bundle
|
|
28
|
+
|
|
29
|
+
Or install it yourself as:
|
|
30
|
+
|
|
31
|
+
$ gem install rack-attack
|
|
32
|
+
|
|
33
|
+
### 2. Plugging into the application
|
|
34
|
+
|
|
35
|
+
Then tell your ruby web application to use rack-attack as a middleware.
|
|
36
|
+
|
|
37
|
+
a) For __rails__ applications:
|
|
26
38
|
|
|
27
39
|
```ruby
|
|
28
40
|
# In config/application.rb
|
|
41
|
+
|
|
29
42
|
config.middleware.use Rack::Attack
|
|
30
43
|
```
|
|
31
44
|
|
|
32
|
-
|
|
45
|
+
b) For __rack__ applications:
|
|
33
46
|
|
|
34
47
|
```ruby
|
|
35
48
|
# In config.ru
|
|
49
|
+
|
|
50
|
+
require "rack/attack"
|
|
36
51
|
use Rack::Attack
|
|
37
52
|
```
|
|
38
53
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class Rack::Attack
|
|
43
|
-
# your custom configuration...
|
|
44
|
-
end
|
|
45
|
-
```
|
|
54
|
+
__IMPORTANT__: By default, rack-attack won't perform any blocking or throttling, until you specifically tell it what to protect against by configuring some rules.
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
46
57
|
|
|
47
58
|
*Tip:* The example in the wiki is a great way to get started:
|
|
48
59
|
[Example Configuration](https://github.com/kickstarter/rack-attack/wiki/Example-Configuration)
|
|
49
60
|
|
|
50
|
-
|
|
61
|
+
Define rules by calling `Rack::Attack` public methods, in any file that runs when your application is being initialized. For rails applications this means creating a new file named `config/initializers/rack_attack.rb` and writing your rules there.
|
|
51
62
|
|
|
52
|
-
|
|
53
|
-
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new # defaults to Rails.cache
|
|
54
|
-
```
|
|
63
|
+
### Safelisting
|
|
55
64
|
|
|
56
|
-
|
|
65
|
+
Safelists have the most precedence, so any request matching a safelist would be allowed despite matching any number of blocklists or throttles.
|
|
57
66
|
|
|
58
|
-
|
|
67
|
+
#### `safelist_ip(ip_address_string)`
|
|
59
68
|
|
|
60
|
-
|
|
69
|
+
E.g.
|
|
61
70
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
* Otherwise, if the request matches any **throttle**, a counter is incremented in the Rack::Attack.cache. If any throttle's limit is exceeded, the request is blocked.
|
|
65
|
-
* Otherwise, all **tracks** are checked, and the request is allowed.
|
|
71
|
+
```ruby
|
|
72
|
+
# config/initializers/rack_attack.rb (for rails app)
|
|
66
73
|
|
|
67
|
-
|
|
74
|
+
Rack::Attack.safelist_ip("5.6.7.8")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### `safelist_ip(ip_subnet_string)`
|
|
78
|
+
|
|
79
|
+
E.g.
|
|
68
80
|
|
|
69
81
|
```ruby
|
|
70
|
-
|
|
71
|
-
req = Rack::Attack::Request.new(env)
|
|
82
|
+
# config/initializers/rack_attack.rb (for rails app)
|
|
72
83
|
|
|
73
|
-
|
|
74
|
-
@app.call(env)
|
|
75
|
-
elsif blocklisted?(req)
|
|
76
|
-
self.class.blocklisted_response.call(env)
|
|
77
|
-
elsif throttled?(req)
|
|
78
|
-
self.class.throttled_response.call(env)
|
|
79
|
-
else
|
|
80
|
-
tracked?(req)
|
|
81
|
-
@app.call(env)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
+
Rack::Attack.safelist_ip("5.6.7.0/24")
|
|
84
85
|
```
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
can cleanly monkey patch helper methods onto the
|
|
88
|
-
[request object](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack/request.rb).
|
|
87
|
+
#### `safelist(name, &block)`
|
|
89
88
|
|
|
90
|
-
|
|
89
|
+
Name your custom safelist and make your ruby-block argument return a truthy value if you want the request to be blocked, and falsy otherwise.
|
|
91
90
|
|
|
92
|
-
|
|
91
|
+
The request object is a [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request).
|
|
93
92
|
|
|
94
|
-
|
|
93
|
+
E.g.
|
|
95
94
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
A [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request) object is passed to the block (named 'req' in the examples).
|
|
95
|
+
```ruby
|
|
96
|
+
# config/initializers/rack_attack.rb (for rails apps)
|
|
99
97
|
|
|
100
|
-
|
|
98
|
+
# Provided that trusted users use an HTTP request header named APIKey
|
|
99
|
+
Rack::Attack.safelist("mark any authenticated access safe") do |request|
|
|
100
|
+
# Requests are allowed if the return value is truthy
|
|
101
|
+
request.env["APIKey"] == "secret-string"
|
|
102
|
+
end
|
|
101
103
|
|
|
102
|
-
```ruby
|
|
103
104
|
# Always allow requests from localhost
|
|
104
105
|
# (blocklist & throttles are skipped)
|
|
105
106
|
Rack::Attack.safelist('allow from localhost') do |req|
|
|
@@ -108,16 +109,44 @@ Rack::Attack.safelist('allow from localhost') do |req|
|
|
|
108
109
|
end
|
|
109
110
|
```
|
|
110
111
|
|
|
111
|
-
###
|
|
112
|
+
### Blocking
|
|
113
|
+
|
|
114
|
+
#### `blocklist_ip(ip_address_string)`
|
|
115
|
+
|
|
116
|
+
E.g.
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
# config/initializers/rack_attack.rb (for rails apps)
|
|
120
|
+
|
|
121
|
+
Rack::Attack.blocklist_ip("1.2.3.4")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### `blocklist_ip(ip_subnet_string)`
|
|
125
|
+
|
|
126
|
+
E.g.
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
# config/initializers/rack_attack.rb (for rails apps)
|
|
130
|
+
|
|
131
|
+
Rack::Attack.blocklist_ip("1.2.0.0/16")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
#### `blocklist(name, &block)`
|
|
135
|
+
|
|
136
|
+
Name your custom blocklist and make your ruby-block argument return a truthy value if you want the request to be blocked, and falsy otherwise.
|
|
137
|
+
|
|
138
|
+
The request object is a [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request).
|
|
139
|
+
|
|
140
|
+
E.g.
|
|
112
141
|
|
|
113
142
|
```ruby
|
|
114
|
-
#
|
|
115
|
-
|
|
143
|
+
# config/initializers/rack_attack.rb (for rails apps)
|
|
144
|
+
|
|
145
|
+
Rack::Attack.blocklist("block all access to admin") do |request|
|
|
116
146
|
# Requests are blocked if the return value is truthy
|
|
117
|
-
|
|
147
|
+
request.path.start_with?("/admin")
|
|
118
148
|
end
|
|
119
149
|
|
|
120
|
-
# Block logins from a bad user agent
|
|
121
150
|
Rack::Attack.blocklist('block bad UA logins') do |req|
|
|
122
151
|
req.path == '/login' && req.post? && req.user_agent == 'BadUA'
|
|
123
152
|
end
|
|
@@ -126,17 +155,19 @@ end
|
|
|
126
155
|
#### Fail2Ban
|
|
127
156
|
|
|
128
157
|
`Fail2Ban.filter` can be used within a blocklist to block all requests from misbehaving clients.
|
|
129
|
-
This pattern is inspired by [fail2ban](
|
|
130
|
-
See the [fail2ban documentation](
|
|
158
|
+
This pattern is inspired by [fail2ban](https://www.fail2ban.org/wiki/index.php/Main_Page).
|
|
159
|
+
See the [fail2ban documentation](https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jail_Options) for more details on
|
|
131
160
|
how the parameters work. For multiple filters, be sure to put each filter in a separate blocklist and use a unique discriminator for each fail2ban filter.
|
|
132
161
|
|
|
162
|
+
Fail2ban state is stored in a [configurable cache](#cache-store-configuration) (which defaults to `Rails.cache` if present).
|
|
163
|
+
|
|
133
164
|
```ruby
|
|
134
165
|
# Block suspicious requests for '/etc/password' or wordpress specific paths.
|
|
135
166
|
# After 3 blocked requests in 10 minutes, block all requests from that IP for 5 minutes.
|
|
136
167
|
Rack::Attack.blocklist('fail2ban pentesters') do |req|
|
|
137
168
|
# `filter` returns truthy value if request fails, or if it's from a previously banned IP
|
|
138
169
|
# so the request is blocked
|
|
139
|
-
Rack::Attack::Fail2Ban.filter("pentesters-#{req.ip}", :
|
|
170
|
+
Rack::Attack::Fail2Ban.filter("pentesters-#{req.ip}", maxretry: 3, findtime: 10.minutes, bantime: 5.minutes) do
|
|
140
171
|
# The count for the IP is incremented if the return value is truthy
|
|
141
172
|
CGI.unescape(req.query_string) =~ %r{/etc/passwd} ||
|
|
142
173
|
req.path.include?('/etc/passwd') ||
|
|
@@ -150,8 +181,12 @@ end
|
|
|
150
181
|
Note that `Fail2Ban` filters are not automatically scoped to the blocklist, so when using multiple filters in an application the scoping must be added to the discriminator e.g. `"pentest:#{req.ip}"`.
|
|
151
182
|
|
|
152
183
|
#### Allow2Ban
|
|
184
|
+
|
|
153
185
|
`Allow2Ban.filter` works the same way as the `Fail2Ban.filter` except that it *allows* requests from misbehaving
|
|
154
186
|
clients until such time as they reach maxretry at which they are cut off as per normal.
|
|
187
|
+
|
|
188
|
+
Allow2ban state is stored in a [configurable cache](#cache-store-configuration) (which defaults to `Rails.cache` if present).
|
|
189
|
+
|
|
155
190
|
```ruby
|
|
156
191
|
# Lockout IP addresses that are hammering your login page.
|
|
157
192
|
# After 20 requests in 1 minute, block all requests from that IP for 1 hour.
|
|
@@ -159,40 +194,47 @@ Rack::Attack.blocklist('allow2ban login scrapers') do |req|
|
|
|
159
194
|
# `filter` returns false value if request is to your login page (but still
|
|
160
195
|
# increments the count) so request below the limit are not blocked until
|
|
161
196
|
# they hit the limit. At that point, filter will return true and block.
|
|
162
|
-
Rack::Attack::Allow2Ban.filter(req.ip, :
|
|
197
|
+
Rack::Attack::Allow2Ban.filter(req.ip, maxretry: 20, findtime: 1.minute, bantime: 1.hour) do
|
|
163
198
|
# The count for the IP is incremented if the return value is truthy.
|
|
164
199
|
req.path == '/login' and req.post?
|
|
165
200
|
end
|
|
166
201
|
end
|
|
167
202
|
```
|
|
168
203
|
|
|
204
|
+
### Throttling
|
|
205
|
+
|
|
206
|
+
Throttle state is stored in a [configurable cache](#cache-store-configuration) (which defaults to `Rails.cache` if present).
|
|
207
|
+
|
|
208
|
+
#### `throttle(name, options, &block)`
|
|
169
209
|
|
|
170
|
-
|
|
210
|
+
Name your custom throttle, provide `limit` and `period` as options, and make your ruby-block argument return the __discriminator__. This discriminator is how you tell rack-attack whether you're limiting per IP address, per user email or any other.
|
|
211
|
+
|
|
212
|
+
The request object is a [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request).
|
|
213
|
+
|
|
214
|
+
E.g.
|
|
171
215
|
|
|
172
216
|
```ruby
|
|
173
|
-
#
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
# "rack::attack:#{Time.now.to_i/1.second}:req/ip:#{req.ip}"
|
|
178
|
-
#
|
|
179
|
-
# If falsy, the cache key is neither incremented nor checked.
|
|
180
|
-
|
|
181
|
-
req.ip
|
|
217
|
+
# config/initializers/rack_attack.rb (for rails apps)
|
|
218
|
+
|
|
219
|
+
Rack::Attack.throttle("requests by ip", limit: 5, period: 2) do |request|
|
|
220
|
+
request.ip
|
|
182
221
|
end
|
|
183
222
|
|
|
184
223
|
# Throttle login attempts for a given email parameter to 6 reqs/minute
|
|
185
224
|
# Return the email as a discriminator on POST /login requests
|
|
186
|
-
Rack::Attack.throttle('logins
|
|
187
|
-
|
|
225
|
+
Rack::Attack.throttle('limit logins per email', limit: 6, period: 60) do |req|
|
|
226
|
+
if req.path == '/login' && req.post?
|
|
227
|
+
req.params['email']
|
|
228
|
+
end
|
|
188
229
|
end
|
|
189
230
|
|
|
190
231
|
# You can also set a limit and period using a proc. For instance, after
|
|
191
232
|
# Rack::Auth::Basic has authenticated the user:
|
|
192
|
-
limit_proc = proc {|req| req.env["REMOTE_USER"] == "admin" ? 100 : 1}
|
|
193
|
-
period_proc = proc {|req| req.env["REMOTE_USER"] == "admin" ? 1
|
|
194
|
-
|
|
195
|
-
|
|
233
|
+
limit_proc = proc { |req| req.env["REMOTE_USER"] == "admin" ? 100 : 1 }
|
|
234
|
+
period_proc = proc { |req| req.env["REMOTE_USER"] == "admin" ? 1 : 60 }
|
|
235
|
+
|
|
236
|
+
Rack::Attack.throttle('request per ip', limit: limit_proc, period: period_proc) do |request|
|
|
237
|
+
request.ip
|
|
196
238
|
end
|
|
197
239
|
```
|
|
198
240
|
|
|
@@ -205,7 +247,7 @@ Rack::Attack.track("special_agent") do |req|
|
|
|
205
247
|
end
|
|
206
248
|
|
|
207
249
|
# Supports optional limit and period, triggers the notification only when the limit is reached.
|
|
208
|
-
Rack::Attack.track("special_agent", :
|
|
250
|
+
Rack::Attack.track("special_agent", limit: 6, period: 60) do |req|
|
|
209
251
|
req.user_agent == "SpecialAgent"
|
|
210
252
|
end
|
|
211
253
|
|
|
@@ -218,9 +260,19 @@ ActiveSupport::Notifications.subscribe("rack.attack") do |name, start, finish, r
|
|
|
218
260
|
end
|
|
219
261
|
```
|
|
220
262
|
|
|
221
|
-
|
|
263
|
+
### Cache store configuration
|
|
264
|
+
|
|
265
|
+
Throttle, allow2ban and fail2ban state is stored in a configurable cache (which defaults to `Rails.cache` if present), presumably backed by memcached or redis ([at least gem v3.0.0](https://rubygems.org/gems/redis)).
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new # defaults to Rails.cache
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Note that `Rack::Attack.cache` is only used for throttling, allow2ban and fail2ban filtering; not blocklisting and safelisting. Your cache store must implement `increment` and `write` like [ActiveSupport::Cache::Store](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html).
|
|
272
|
+
|
|
273
|
+
## Customizing responses
|
|
222
274
|
|
|
223
|
-
Customize the response of blocklisted and throttled requests using an object that adheres to the [Rack app interface](http://
|
|
275
|
+
Customize the response of blocklisted and throttled requests using an object that adheres to the [Rack app interface](http://www.rubydoc.info/github/rack/rack/file/SPEC).
|
|
224
276
|
|
|
225
277
|
```ruby
|
|
226
278
|
Rack::Attack.blocklisted_response = lambda do |env|
|
|
@@ -233,7 +285,8 @@ Rack::Attack.throttled_response = lambda do |env|
|
|
|
233
285
|
# NB: you have access to the name and other data about the matched throttle
|
|
234
286
|
# env['rack.attack.matched'],
|
|
235
287
|
# env['rack.attack.match_type'],
|
|
236
|
-
# env['rack.attack.match_data']
|
|
288
|
+
# env['rack.attack.match_data'],
|
|
289
|
+
# env['rack.attack.match_discriminator']
|
|
237
290
|
|
|
238
291
|
# Using 503 because it may make attacker think that they have successfully
|
|
239
292
|
# DOSed the site. Rack::Attack returns 429 for throttling by default
|
|
@@ -250,13 +303,13 @@ Here's an example response that includes conventional `X-RateLimit-*` headers:
|
|
|
250
303
|
|
|
251
304
|
```ruby
|
|
252
305
|
Rack::Attack.throttled_response = lambda do |env|
|
|
253
|
-
now = Time.now
|
|
254
306
|
match_data = env['rack.attack.match_data']
|
|
307
|
+
now = match_data[:epoch_time]
|
|
255
308
|
|
|
256
309
|
headers = {
|
|
257
310
|
'X-RateLimit-Limit' => match_data[:limit].to_s,
|
|
258
311
|
'X-RateLimit-Remaining' => '0',
|
|
259
|
-
'X-RateLimit-Reset' => (now + (match_data[:period] - now
|
|
312
|
+
'X-RateLimit-Reset' => (now + (match_data[:period] - now % match_data[:period])).to_s
|
|
260
313
|
}
|
|
261
314
|
|
|
262
315
|
[ 429, headers, ["Throttled\n"]]
|
|
@@ -267,7 +320,7 @@ end
|
|
|
267
320
|
For responses that did not exceed a throttle limit, Rack::Attack annotates the env with match data:
|
|
268
321
|
|
|
269
322
|
```ruby
|
|
270
|
-
request.env['rack.attack.throttle_data'][name] # => { :count => n, :period => p, :limit => l }
|
|
323
|
+
request.env['rack.attack.throttle_data'][name] # => { :count => n, :period => p, :limit => l, :epoch_time => t }
|
|
271
324
|
```
|
|
272
325
|
|
|
273
326
|
## Logging & Instrumentation
|
|
@@ -282,6 +335,43 @@ ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, r
|
|
|
282
335
|
end
|
|
283
336
|
```
|
|
284
337
|
|
|
338
|
+
## How it works
|
|
339
|
+
|
|
340
|
+
The Rack::Attack middleware compares each request against *safelists*, *blocklists*, *throttles*, and *tracks* that you define. There are none by default.
|
|
341
|
+
|
|
342
|
+
* If the request matches any **safelist**, it is allowed.
|
|
343
|
+
* Otherwise, if the request matches any **blocklist**, it is blocked.
|
|
344
|
+
* Otherwise, if the request matches any **throttle**, a counter is incremented in the Rack::Attack.cache. If any throttle's limit is exceeded, the request is blocked.
|
|
345
|
+
* Otherwise, all **tracks** are checked, and the request is allowed.
|
|
346
|
+
|
|
347
|
+
The algorithm is actually more concise in code: See [Rack::Attack.call](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack.rb):
|
|
348
|
+
|
|
349
|
+
```ruby
|
|
350
|
+
def call(env)
|
|
351
|
+
req = Rack::Attack::Request.new(env)
|
|
352
|
+
|
|
353
|
+
if safelisted?(req)
|
|
354
|
+
@app.call(env)
|
|
355
|
+
elsif blocklisted?(req)
|
|
356
|
+
self.class.blocklisted_response.call(env)
|
|
357
|
+
elsif throttled?(req)
|
|
358
|
+
self.class.throttled_response.call(env)
|
|
359
|
+
else
|
|
360
|
+
tracked?(req)
|
|
361
|
+
@app.call(env)
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Note: `Rack::Attack::Request` is just a subclass of `Rack::Request` so that you
|
|
367
|
+
can cleanly monkey patch helper methods onto the
|
|
368
|
+
[request object](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack/request.rb).
|
|
369
|
+
|
|
370
|
+
### About Tracks
|
|
371
|
+
|
|
372
|
+
`Rack::Attack.track` doesn't affect request processing. Tracks are an easy way to log and measure requests matching arbitrary attributes.
|
|
373
|
+
|
|
374
|
+
|
|
285
375
|
## Testing
|
|
286
376
|
|
|
287
377
|
A note on developing and testing apps using Rack::Attack - if you are using throttling in particular, you will
|
|
@@ -298,7 +388,7 @@ so try to keep the number of throttle checks per request low.
|
|
|
298
388
|
If a request is blocklisted or throttled, the response is a very simple Rack response.
|
|
299
389
|
A single typical ruby web server thread can block several hundred requests per second.
|
|
300
390
|
|
|
301
|
-
Rack::Attack complements tools like `iptables` and nginx's [limit_conn_zone module](
|
|
391
|
+
Rack::Attack complements tools like `iptables` and nginx's [limit_conn_zone module](https://nginx.org/en/docs/http/ngx_http_limit_conn_module.html#limit_conn_zone).
|
|
302
392
|
|
|
303
393
|
## Motivation
|
|
304
394
|
|
|
@@ -312,9 +402,15 @@ less on short-term, one-off hacks to block a particular attack.
|
|
|
312
402
|
|
|
313
403
|
## Contributing
|
|
314
404
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
405
|
+
Check out the [Contributing guide](CONTRIBUTING.md).
|
|
406
|
+
|
|
407
|
+
## Code of Conduct
|
|
408
|
+
|
|
409
|
+
This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Code of Conduct](CODE_OF_CONDUCT.md).
|
|
410
|
+
|
|
411
|
+
## Development setup
|
|
412
|
+
|
|
413
|
+
Check out the [Development guide](docs/development.md).
|
|
318
414
|
|
|
319
415
|
## Mailing list
|
|
320
416
|
|
|
@@ -327,4 +423,4 @@ New releases of Rack::Attack are announced on
|
|
|
327
423
|
|
|
328
424
|
Copyright Kickstarter, PBC.
|
|
329
425
|
|
|
330
|
-
Released under an [MIT License](
|
|
426
|
+
Released under an [MIT License](https://opensource.org/licenses/MIT).
|