rack-attack 4.3.1 → 5.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +230 -113
- data/Rakefile +11 -3
- data/bin/setup +8 -0
- data/lib/rack/attack.rb +121 -48
- data/lib/rack/attack/allow2ban.rb +2 -1
- data/lib/rack/attack/{whitelist.rb → blocklist.rb} +2 -3
- data/lib/rack/attack/cache.rb +24 -5
- data/lib/rack/attack/check.rb +6 -8
- data/lib/rack/attack/fail2ban.rb +3 -2
- data/lib/rack/attack/path_normalizer.rb +6 -11
- data/lib/rack/attack/request.rb +1 -1
- data/lib/rack/attack/{blacklist.rb → safelist.rb} +2 -4
- data/lib/rack/attack/store_proxy.rb +13 -12
- data/lib/rack/attack/store_proxy/dalli_proxy.rb +2 -3
- data/lib/rack/attack/store_proxy/mem_cache_proxy.rb +50 -0
- 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 +10 -9
- data/spec/fail2ban_spec.rb +12 -10
- 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 +2 -2
- data/spec/rack_attack_spec.rb +53 -18
- data/spec/rack_attack_throttle_spec.rb +45 -13
- 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 +161 -61
- data/spec/integration/rack_attack_cache_spec.rb +0 -119
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,198 +1,240 @@
|
|
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 *whitelisting*, *blacklisting*, *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
|
-
[![Build Status](https://travis-ci.org/kickstarter/rack-attack.png?branch=master)](https://travis-ci.org/kickstarter/rack-attack)
|
13
|
-
[![Code Climate](https://codeclimate.com/github/kickstarter/rack-attack.png)](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
|
+
[![Gem Version](https://badge.fury.io/rb/rack-attack.svg)](https://badge.fury.io/rb/rack-attack)
|
10
|
+
[![Build Status](https://travis-ci.org/kickstarter/rack-attack.svg?branch=master)](https://travis-ci.org/kickstarter/rack-attack)
|
11
|
+
[![Code Climate](https://codeclimate.com/github/kickstarter/rack-attack.svg)](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 blacklisted?(req)
|
76
|
-
self.class.blacklisted_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
|
-
Rack::Attack.
|
105
|
+
# (blocklist & throttles are skipped)
|
106
|
+
Rack::Attack.safelist('allow from localhost') do |req|
|
106
107
|
# Requests are allowed if the return value is truthy
|
107
108
|
'127.0.0.1' == req.ip || '::1' == req.ip
|
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
|
-
|
121
|
-
Rack::Attack.blacklist('block bad UA logins') do |req|
|
150
|
+
Rack::Attack.blocklist('block bad UA logins') do |req|
|
122
151
|
req.path == '/login' && req.post? && req.user_agent == 'BadUA'
|
123
152
|
end
|
124
153
|
```
|
125
154
|
|
126
155
|
#### Fail2Ban
|
127
156
|
|
128
|
-
`Fail2Ban.filter` can be used within a
|
129
|
-
This pattern is inspired by [fail2ban](
|
130
|
-
See the [fail2ban documentation](
|
131
|
-
how the parameters work. For multiple filters, be sure to put each filter in a separate
|
157
|
+
`Fail2Ban.filter` can be used within a blocklist to block all requests from misbehaving clients.
|
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
|
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.
|
161
|
+
|
162
|
+
Fail2ban state is stored in a [configurable cache](#cache-store-configuration) (which defaults to `Rails.cache` if present).
|
132
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
|
-
Rack::Attack.
|
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
|
-
CGI.unescape(req.query_string) =~ %r{/etc/passwd} ||
|
172
|
+
CGI.unescape(req.query_string) =~ %r{/etc/passwd} ||
|
142
173
|
req.path.include?('/etc/passwd') ||
|
143
|
-
req.path.include?('wp-admin') ||
|
174
|
+
req.path.include?('wp-admin') ||
|
144
175
|
req.path.include?('wp-login')
|
145
|
-
|
176
|
+
|
146
177
|
end
|
147
178
|
end
|
148
179
|
```
|
149
180
|
|
150
|
-
Note that `Fail2Ban` filters are not automatically scoped to the
|
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.
|
158
|
-
Rack::Attack.
|
193
|
+
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,35 +260,67 @@ 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
|
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
|
-
Rack::Attack.
|
278
|
+
Rack::Attack.blocklisted_response = lambda do |env|
|
227
279
|
# Using 503 because it may make attacker think that they have successfully
|
228
|
-
# DOSed the site. Rack::Attack returns 403 for
|
280
|
+
# DOSed the site. Rack::Attack returns 403 for blocklists by default
|
229
281
|
[ 503, {}, ['Blocked']]
|
230
282
|
end
|
231
283
|
|
232
284
|
Rack::Attack.throttled_response = lambda do |env|
|
233
|
-
# name and other data about the matched throttle
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
].inspect
|
285
|
+
# NB: you have access to the name and other data about the matched throttle
|
286
|
+
# env['rack.attack.matched'],
|
287
|
+
# env['rack.attack.match_type'],
|
288
|
+
# env['rack.attack.match_data'],
|
289
|
+
# env['rack.attack.match_discriminator']
|
239
290
|
|
240
291
|
# Using 503 because it may make attacker think that they have successfully
|
241
292
|
# DOSed the site. Rack::Attack returns 429 for throttling by default
|
242
|
-
[ 503, {}, [
|
293
|
+
[ 503, {}, ["Server Error\n"]]
|
294
|
+
end
|
295
|
+
```
|
296
|
+
|
297
|
+
### X-RateLimit headers for well-behaved clients
|
298
|
+
|
299
|
+
While Rack::Attack's primary focus is minimizing harm from abusive clients, it
|
300
|
+
can also be used to return rate limit data that's helpful for well-behaved clients.
|
301
|
+
|
302
|
+
Here's an example response that includes conventional `X-RateLimit-*` headers:
|
303
|
+
|
304
|
+
```ruby
|
305
|
+
Rack::Attack.throttled_response = lambda do |env|
|
306
|
+
match_data = env['rack.attack.match_data']
|
307
|
+
now = match_data[:epoch_time]
|
308
|
+
|
309
|
+
headers = {
|
310
|
+
'X-RateLimit-Limit' => match_data[:limit].to_s,
|
311
|
+
'X-RateLimit-Remaining' => '0',
|
312
|
+
'X-RateLimit-Reset' => (now + (match_data[:period] - now % match_data[:period])).to_s
|
313
|
+
}
|
314
|
+
|
315
|
+
[ 429, headers, ["Throttled\n"]]
|
243
316
|
end
|
244
317
|
```
|
245
318
|
|
319
|
+
|
246
320
|
For responses that did not exceed a throttle limit, Rack::Attack annotates the env with match data:
|
247
321
|
|
248
322
|
```ruby
|
249
|
-
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 }
|
250
324
|
```
|
251
325
|
|
252
326
|
## Logging & Instrumentation
|
@@ -261,6 +335,43 @@ ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, r
|
|
261
335
|
end
|
262
336
|
```
|
263
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
|
+
|
264
375
|
## Testing
|
265
376
|
|
266
377
|
A note on developing and testing apps using Rack::Attack - if you are using throttling in particular, you will
|
@@ -274,10 +385,10 @@ but it depends on how many checks you've configured, and how long they take.
|
|
274
385
|
Throttles usually require a network roundtrip to your cache server(s),
|
275
386
|
so try to keep the number of throttle checks per request low.
|
276
387
|
|
277
|
-
If a request is
|
388
|
+
If a request is blocklisted or throttled, the response is a very simple Rack response.
|
278
389
|
A single typical ruby web server thread can block several hundred requests per second.
|
279
390
|
|
280
|
-
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).
|
281
392
|
|
282
393
|
## Motivation
|
283
394
|
|
@@ -291,9 +402,15 @@ less on short-term, one-off hacks to block a particular attack.
|
|
291
402
|
|
292
403
|
## Contributing
|
293
404
|
|
294
|
-
|
295
|
-
|
296
|
-
|
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).
|
297
414
|
|
298
415
|
## Mailing list
|
299
416
|
|
@@ -304,6 +421,6 @@ New releases of Rack::Attack are announced on
|
|
304
421
|
|
305
422
|
## License
|
306
423
|
|
307
|
-
Copyright Kickstarter,
|
424
|
+
Copyright Kickstarter, PBC.
|
308
425
|
|
309
|
-
Released under an [MIT License](
|
426
|
+
Released under an [MIT License](https://opensource.org/licenses/MIT).
|