rack-attack 4.3.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 +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
|
-
[](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 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).
|