rack-attack 5.1.0 → 5.2.0

Sign up to get free protection for your applications and to get access to all the features.
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