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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6adfaa8597fafd710ea09558be12046bf35eb3816a2cb81c275e07f12c3ebc9f
4
- data.tar.gz: 9e256c9b94880a4118f47d3bec2826bdb1b0d6fb501567384cd3a8049b44fa86
3
+ metadata.gz: 21a52aca7aa6592b23e6a3e99f0b06cf6d4d9eedb8366ec57fe4f9cfe804ea82
4
+ data.tar.gz: 32e0db149bc10308fb8b5ae737147e1c11c9f854e08b9f453d9a7066911308fe
5
5
  SHA512:
6
- metadata.gz: ab110884b8a75cec12ce734457a5e71f1f04351ad21f6ce160dbfda8161c642521e23c6d0d655dd03f51bafffecdff188cb89d88bf2b2e3b40e109bbf1ebffa2
7
- data.tar.gz: c9e46912da6c457bc53970122c4cfc3afab4977037bf52c1dcd5ba9502ba6d83c0137b939d217645df347d2ac1cbec1b796f0d114bae2d0a668e4850dcc018b2
6
+ metadata.gz: 560d951d375a9114752b37a2858f48e85af9edcdeabb0073c6e6f8d179b6d10a0331a7abee625df4c0171f9c993bb105cc8e54fed9170fd47d288fb2bf29617a
7
+ data.tar.gz: b3993951744e2755873cc7ce0c6810c3780ad44c6c84491bda5277a46536589d15b26bd25e7849e7b20a4e28aebc86ed874cb0b61e0d988da54252a3b3e024dc
data/README.md CHANGED
@@ -1,10 +1,8 @@
1
- # Rack::Attack!!!
2
- *Rack middleware for blocking & throttling abusive requests*
1
+ # Rack::Attack
3
2
 
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.
3
+ *Rack middleware for blocking & throttling abusive requests*
6
4
 
7
- Throttle and fail2ban state is stored in a configurable cache (e.g. `Rails.cache`), presumably backed by memcached or redis ([at least gem v3.0.0](https://rubygems.org/gems/redis)).
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.
8
6
 
9
7
  See the [Backing & Hacking blog post](http://www.kickstarter.com/backing-and-hacking/rack-attack-protection-from-abusive-clients) introducing Rack::Attack.
10
8
 
@@ -12,98 +10,97 @@ See the [Backing & Hacking blog post](http://www.kickstarter.com/backing-and-hac
12
10
  [![Build Status](https://travis-ci.org/kickstarter/rack-attack.svg?branch=master)](https://travis-ci.org/kickstarter/rack-attack)
13
11
  [![Code Climate](https://codeclimate.com/github/kickstarter/rack-attack.svg)](https://codeclimate.com/github/kickstarter/rack-attack)
14
12
 
15
- ## Looking for maintainers
16
-
17
- I'm looking for new maintainers to help me support Rack::Attack. Check out
18
- [issue #219 for details](https://github.com/kickstarter/rack-attack/issues/219).
19
-
20
13
  ## Getting started
21
14
 
22
- Install the [rack-attack](http://rubygems.org/gems/rack-attack) gem; or add it to your Gemfile with bundler:
15
+ ### 1. Installing
16
+
17
+ Add this line to your application's Gemfile:
23
18
 
24
19
  ```ruby
25
20
  # In your Gemfile
21
+
26
22
  gem 'rack-attack'
27
23
  ```
28
- Tell your app to use the Rack::Attack middleware.
29
- For Rails apps:
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:
30
38
 
31
39
  ```ruby
32
40
  # In config/application.rb
41
+
33
42
  config.middleware.use Rack::Attack
34
43
  ```
35
44
 
36
- Or for Rackup files:
45
+ b) For __rack__ applications:
37
46
 
38
47
  ```ruby
39
48
  # In config.ru
49
+
50
+ require "rack/attack"
40
51
  use Rack::Attack
41
52
  ```
42
53
 
43
- Add a `rack_attack.rb` file to `config/initializers/`:
44
- ```ruby
45
- # In config/initializers/rack_attack.rb
46
- class Rack::Attack
47
- # your custom configuration...
48
- end
49
- ```
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
50
57
 
51
58
  *Tip:* The example in the wiki is a great way to get started:
52
59
  [Example Configuration](https://github.com/kickstarter/rack-attack/wiki/Example-Configuration)
53
60
 
54
- Optionally configure the cache store for throttling or fail2ban filtering:
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.
55
62
 
56
- ```ruby
57
- Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new # defaults to Rails.cache
58
- ```
63
+ ### Safelisting
59
64
 
60
- Note that `Rack::Attack.cache` is only used for throttling and fail2ban filtering; not blocklisting & safelisting. Your cache store must implement `increment` and `write` like [ActiveSupport::Cache::Store](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html).
65
+ Safelists have the most precedence, so any request matching a safelist would be allowed despite matching any number of blocklists or throttles.
61
66
 
62
- ## How it works
67
+ #### `safelist_ip(ip_address_string)`
63
68
 
64
- The Rack::Attack middleware compares each request against *safelists*, *blocklists*, *throttles*, and *tracks* that you define. There are none by default.
69
+ E.g.
65
70
 
66
- * If the request matches any **safelist**, it is allowed.
67
- * Otherwise, if the request matches any **blocklist**, it is blocked.
68
- * 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.
69
- * Otherwise, all **tracks** are checked, and the request is allowed.
71
+ ```ruby
72
+ # config/initializers/rack_attack.rb (for rails app)
70
73
 
71
- The algorithm is actually more concise in code: See [Rack::Attack.call](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack.rb):
74
+ Rack::Attack.safelist_ip("5.6.7.8")
75
+ ```
76
+
77
+ #### `safelist_ip(ip_subnet_string)`
78
+
79
+ E.g.
72
80
 
73
81
  ```ruby
74
- def call(env)
75
- req = Rack::Attack::Request.new(env)
82
+ # config/initializers/rack_attack.rb (for rails app)
76
83
 
77
- if safelisted?(req)
78
- @app.call(env)
79
- elsif blocklisted?(req)
80
- self.class.blocklisted_response.call(env)
81
- elsif throttled?(req)
82
- self.class.throttled_response.call(env)
83
- else
84
- tracked?(req)
85
- @app.call(env)
86
- end
87
- end
84
+ Rack::Attack.safelist_ip("5.6.7.0/24")
88
85
  ```
89
86
 
90
- Note: `Rack::Attack::Request` is just a subclass of `Rack::Request` so that you
91
- can cleanly monkey patch helper methods onto the
92
- [request object](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack/request.rb).
87
+ #### `safelist(name, &block)`
93
88
 
94
- ## About Tracks
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.
95
90
 
96
- `Rack::Attack.track` doesn't affect request processing. Tracks are an easy way to log and measure requests matching arbitrary attributes.
91
+ The request object is a [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request).
97
92
 
98
- ## Usage
93
+ E.g.
99
94
 
100
- Define safelists, blocklists, throttles, and tracks as blocks that return truthy values if matched, falsy otherwise. In a Rails app
101
- these go in an initializer in `config/initializers/`.
102
- 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)
103
97
 
104
- ### Safelists
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
105
103
 
106
- ```ruby
107
104
  # Always allow requests from localhost
108
105
  # (blocklist & throttles are skipped)
109
106
  Rack::Attack.safelist('allow from localhost') do |req|
@@ -112,16 +109,44 @@ Rack::Attack.safelist('allow from localhost') do |req|
112
109
  end
113
110
  ```
114
111
 
115
- ### Blocklists
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.
116
127
 
117
128
  ```ruby
118
- # Block requests from 1.2.3.4
119
- Rack::Attack.blocklist('block 1.2.3.4') do |req|
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 returna 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.
141
+
142
+ ```ruby
143
+ # config/initializers/rack_attack.rb (for rails apps)
144
+
145
+ Rack::Attack.blocklist("block all access to admin") do |request|
120
146
  # Requests are blocked if the return value is truthy
121
- '1.2.3.4' == req.ip
147
+ request.path.start_with?("/admin")
122
148
  end
123
149
 
124
- # Block logins from a bad user agent
125
150
  Rack::Attack.blocklist('block bad UA logins') do |req|
126
151
  req.path == '/login' && req.post? && req.user_agent == 'BadUA'
127
152
  end
@@ -134,6 +159,8 @@ This pattern is inspired by [fail2ban](http://www.fail2ban.org/wiki/index.php/Ma
134
159
  See the [fail2ban documentation](http://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jail_Options) for more details on
135
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.
136
161
 
162
+ Fail2ban state is stored in a [configurable cache](#cache-store-configuration) (which defaults to `Rails.cache` if present).
163
+
137
164
  ```ruby
138
165
  # Block suspicious requests for '/etc/password' or wordpress specific paths.
139
166
  # After 3 blocked requests in 10 minutes, block all requests from that IP for 5 minutes.
@@ -154,8 +181,12 @@ end
154
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}"`.
155
182
 
156
183
  #### Allow2Ban
184
+
157
185
  `Allow2Ban.filter` works the same way as the `Fail2Ban.filter` except that it *allows* requests from misbehaving
158
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
+
159
190
  ```ruby
160
191
  # Lockout IP addresses that are hammering your login page.
161
192
  # After 20 requests in 1 minute, block all requests from that IP for 1 hour.
@@ -170,33 +201,40 @@ Rack::Attack.blocklist('allow2ban login scrapers') do |req|
170
201
  end
171
202
  ```
172
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)`
209
+
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.
173
211
 
174
- ### Throttles
212
+ The request object is a [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request).
213
+
214
+ E.g.
175
215
 
176
216
  ```ruby
177
- # Throttle requests to 5 requests per second per ip
178
- Rack::Attack.throttle('req/ip', limit: 5, period: 1.second) do |req|
179
- # If the return value is truthy, the cache key for the return value
180
- # is incremented and compared with the limit. In this case:
181
- # "rack::attack:#{Time.now.to_i/1.second}:req/ip:#{req.ip}"
182
- #
183
- # If falsy, the cache key is neither incremented nor checked.
184
-
185
- 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
186
221
  end
187
222
 
188
223
  # Throttle login attempts for a given email parameter to 6 reqs/minute
189
224
  # Return the email as a discriminator on POST /login requests
190
- Rack::Attack.throttle('logins/email', limit: 6, period: 60) do |req|
191
- req.params['email'] if req.path == '/login' && req.post?
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
192
229
  end
193
230
 
194
231
  # You can also set a limit and period using a proc. For instance, after
195
232
  # Rack::Auth::Basic has authenticated the user:
196
- limit_proc = proc {|req| req.env["REMOTE_USER"] == "admin" ? 100 : 1}
197
- period_proc = proc {|req| req.env["REMOTE_USER"] == "admin" ? 1.second : 1.minute}
198
- Rack::Attack.throttle('req/ip', limit: limit_proc, period: period_proc) do |req|
199
- req.ip
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
200
238
  end
201
239
  ```
202
240
 
@@ -222,7 +260,17 @@ ActiveSupport::Notifications.subscribe("rack.attack") do |name, start, finish, r
222
260
  end
223
261
  ```
224
262
 
225
- ## Responses
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
226
274
 
227
275
  Customize the response of blocklisted and throttled requests using an object that adheres to the [Rack app interface](http://rack.rubyforge.org/doc/SPEC.html).
228
276
 
@@ -287,6 +335,43 @@ ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, r
287
335
  end
288
336
  ```
289
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
+
290
375
  ## Testing
291
376
 
292
377
  A note on developing and testing apps using Rack::Attack - if you are using throttling in particular, you will
@@ -1,12 +1,14 @@
1
1
  require 'rack'
2
2
  require 'forwardable'
3
+ require 'rack/attack/path_normalizer'
4
+ require 'rack/attack/request'
5
+ require "ipaddr"
3
6
 
4
7
  class Rack::Attack
5
8
  class MisconfiguredStoreError < StandardError; end
6
9
  class MissingStoreError < StandardError; end
7
10
 
8
11
  autoload :Cache, 'rack/attack/cache'
9
- autoload :PathNormalizer, 'rack/attack/path_normalizer'
10
12
  autoload :Check, 'rack/attack/check'
11
13
  autoload :Throttle, 'rack/attack/throttle'
12
14
  autoload :Safelist, 'rack/attack/safelist'
@@ -18,7 +20,6 @@ class Rack::Attack
18
20
  autoload :RedisStoreProxy, 'rack/attack/store_proxy/redis_store_proxy'
19
21
  autoload :Fail2Ban, 'rack/attack/fail2ban'
20
22
  autoload :Allow2Ban, 'rack/attack/allow2ban'
21
- autoload :Request, 'rack/attack/request'
22
23
 
23
24
  class << self
24
25
 
@@ -37,6 +38,18 @@ class Rack::Attack
37
38
  self.blocklists[name] = Blocklist.new(name, block)
38
39
  end
39
40
 
41
+ def blocklist_ip(ip)
42
+ @ip_blocklists ||= []
43
+ ip_blocklist_proc = lambda { |request| IPAddr.new(ip).include?(IPAddr.new(request.ip)) }
44
+ @ip_blocklists << Blocklist.new(nil, ip_blocklist_proc)
45
+ end
46
+
47
+ def safelist_ip(ip)
48
+ @ip_safelists ||= []
49
+ ip_safelist_proc = lambda { |request| IPAddr.new(ip).include?(IPAddr.new(request.ip)) }
50
+ @ip_safelists << Safelist.new(nil, ip_safelist_proc)
51
+ end
52
+
40
53
  def blacklist(name, &block)
41
54
  warn "[DEPRECATION] 'Rack::Attack.blacklist' is deprecated. Please use 'blocklist' instead."
42
55
  blocklist(name, &block)
@@ -66,9 +79,8 @@ class Rack::Attack
66
79
  end
67
80
 
68
81
  def safelisted?(req)
69
- safelists.any? do |name, safelist|
70
- safelist[req]
71
- end
82
+ ip_safelists.any? { |safelist| safelist.match?(req) } ||
83
+ safelists.any? { |_name, safelist| safelist.match?(req) }
72
84
  end
73
85
 
74
86
  def whitelisted?(req)
@@ -77,9 +89,8 @@ class Rack::Attack
77
89
  end
78
90
 
79
91
  def blocklisted?(req)
80
- blocklists.any? do |name, blocklist|
81
- blocklist[req]
82
- end
92
+ ip_blocklists.any? { |blocklist| blocklist.match?(req) } ||
93
+ blocklists.any? { |_name, blocklist| blocklist.match?(req) }
83
94
  end
84
95
 
85
96
  def blacklisted?(req)
@@ -109,6 +120,8 @@ class Rack::Attack
109
120
 
110
121
  def clear!
111
122
  @safelists, @blocklists, @throttles, @tracks = {}, {}, {}, {}
123
+ @ip_blocklists = []
124
+ @ip_safelists = []
112
125
  end
113
126
 
114
127
  def blacklisted_response=(res)
@@ -121,6 +134,15 @@ class Rack::Attack
121
134
  blocklisted_response
122
135
  end
123
136
 
137
+ private
138
+
139
+ def ip_blocklists
140
+ @ip_blocklists ||= []
141
+ end
142
+
143
+ def ip_safelists
144
+ @ip_safelists ||= []
145
+ end
124
146
  end
125
147
 
126
148
  # Set defaults