wkimeria-rack-attack 4.1.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.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ODBkODliMDY4NmQ1MzAyZmNlMWZjOWU2ZWQ2YmQwOGU3MjY0MzY4NA==
5
+ data.tar.gz: !binary |-
6
+ YTIxNWEyMzk4MTE0MDEwODQyMTdlYjY4YTc1M2FiZDhiYjQ3YmFjOQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ MjUzMTMyMWJiOWM5MWYzMTI0Zjg3NzYyMDM4M2FhM2RmNjkxMjJjMjViMGRi
10
+ ZWJjMjNjYzc4MDBjZjY1NzkzOWM5ZDA3NWNlYjQ0NThiMTA1OTg0OWE5ZmFh
11
+ N2RmYjdmMDJlYjVhYjMyMDdjN2M3MmRkOGIzYTYzYTVhMTJiMGY=
12
+ data.tar.gz: !binary |-
13
+ NzdmNmE4NDhlNGJkYjgzNTMwYjQzYjlkMzM1YTQ4NjJhYzllZGJkZDE0YTM5
14
+ YWVlYjFhNDBkYjFhZjRiOGYwNjM4YzA1MWQ3MjlhYmUyYzEzZjIzN2EwM2U5
15
+ OGMyMjIwMTdlZGI4OWE3Zjc0ZTdjOTFkZTIyZmIyYTM3ZTc0OGE=
@@ -0,0 +1,308 @@
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.
6
+
7
+ Throttle 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)).
8
+
9
+ See the [Backing & Hacking blog post](http://www.kickstarter.com/backing-and-hacking/rack-attack-protection-from-abusive-clients) introducing Rack::Attack.
10
+
11
+ [![Gem Version](https://badge.fury.io/rb/rack-attack.png)](http://badge.fury.io/rb/rack-attack)
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)
14
+
15
+
16
+ ## Getting started
17
+
18
+ Install the [rack-attack](http://rubygems.org/gems/rack-attack) gem; or add it to you Gemfile with bundler:
19
+
20
+ ```ruby
21
+ # In your Gemfile
22
+ gem 'rack-attack'
23
+ ```
24
+ Tell your app to use the Rack::Attack middleware.
25
+ For Rails 3+ apps:
26
+
27
+ ```ruby
28
+ # In config/application.rb
29
+ config.middleware.use Rack::Attack
30
+ ```
31
+
32
+ Or for Rackup files:
33
+
34
+ ```ruby
35
+ # In config.ru
36
+ use Rack::Attack
37
+ ```
38
+
39
+ Add a `rack-attack.rb` file to `config/initalizers/`:
40
+ ```ruby
41
+ # In config/initializers/rack-attack.rb
42
+ class Rack::Attack
43
+ # your custom configuration...
44
+ end
45
+ ```
46
+
47
+ *Tip:* The example in the wiki is a great way to get started:
48
+ [Example Configuration](https://github.com/kickstarter/rack-attack/wiki/Example-Configuration)
49
+
50
+ Optionally configure the cache store for throttling:
51
+
52
+ ```ruby
53
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new # defaults to Rails.cache
54
+ ```
55
+
56
+ Note that `Rack::Attack.cache` is only used for throttling; not blacklisting & whitelisting. Your cache store must implement `increment` and `write` like [ActiveSupport::Cache::Store](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html).
57
+
58
+ ## How it works
59
+
60
+ The Rack::Attack middleware compares each request against *whitelists*, *blacklists*, *throttles*, and *tracks* that you define. There are none by default.
61
+
62
+ * If the request matches any **whitelist**, it is allowed.
63
+ * Otherwise, if the request matches any **blacklist**, it is blocked.
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.
66
+
67
+ The algorithm is actually more concise in code: See [Rack::Attack.call](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack.rb):
68
+
69
+ ```ruby
70
+ def call(env)
71
+ req = Rack::Attack::Request.new(env)
72
+
73
+ if whitelisted?(req)
74
+ @app.call(env)
75
+ elsif blacklisted?(req)
76
+ blacklisted_response[env]
77
+ elsif throttled?(req)
78
+ throttled_response[env]
79
+ else
80
+ tracked?(req)
81
+ @app.call(env)
82
+ end
83
+ end
84
+ ```
85
+
86
+ Note: `Rack::Attack::Request` is just a subclass of `Rack::Attack` so that you
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).
89
+
90
+ ## About Tracks
91
+
92
+ `Rack::Attack.track` doesn't affect request processing. Tracks are an easy way to log and measure requests matching arbitrary attributes.
93
+
94
+ ## Usage
95
+
96
+ Define whitelists, blacklists, throttles, and tracks as blocks that return truthy values if matched, falsy otherwise. In a Rails app
97
+ these go in an initializer in `config/initializers/`.
98
+ A [Rack::Request](http://rack.rubyforge.org/doc/classes/Rack/Request.html) object is passed to the block (named 'req' in the examples).
99
+
100
+ ### Whitelists
101
+
102
+ ```ruby
103
+ # Always allow requests from localhost
104
+ # (blacklist & throttles are skipped)
105
+ Rack::Attack.whitelist('allow from localhost') do |req|
106
+ # Requests are allowed if the return value is truthy
107
+ '127.0.0.1' == req.ip
108
+ end
109
+ ```
110
+
111
+ ### Blacklists
112
+
113
+ ```ruby
114
+ # Block requests from 1.2.3.4
115
+ Rack::Attack.blacklist('block 1.2.3.4') do |req|
116
+ # Requests are blocked if the return value is truthy
117
+ '1.2.3.4' == req.ip
118
+ end
119
+
120
+ # Block logins from a bad user agent
121
+ Rack::Attack.blacklist('block bad UA logins') do |req|
122
+ req.path == '/login' && req.post? && req.user_agent == 'BadUA'
123
+ end
124
+ ```
125
+
126
+ #### Fail2Ban
127
+
128
+ `Fail2Ban.filter` can be used within a blacklist to block all requests from misbehaving clients.
129
+ This pattern is inspired by [fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page).
130
+ See the [fail2ban documentation](http://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jail_Options) for more details on
131
+ how the parameters work.
132
+
133
+ ```ruby
134
+ # Block requests containing '/etc/password' in the params.
135
+ # After 3 blocked requests in 10 minutes, block all requests from that IP for 5 minutes.
136
+ Rack::Attack.blacklist('fail2ban pentesters') do |req|
137
+ # `filter` returns truthy value if request fails, or if it's from a previously banned IP
138
+ # so the request is blocked
139
+ Rack::Attack::Fail2Ban.filter(req.ip, :maxretry => 3, :findtime => 10.minutes, :bantime => 5.minutes) do
140
+ # The count for the IP is incremented if the return value is truthy.
141
+ CGI.unescape(req.query_string) =~ %r{/etc/passwd}
142
+ end
143
+ end
144
+ ```
145
+
146
+ #### Allow2Ban
147
+ `Allow2Ban.filter` works the same way as the `Fail2Ban.filter` except that it *allows* requests from misbehaving
148
+ clients until such time as they reach maxretry at which they are cut off as per normal.
149
+ ```ruby
150
+ # Lockout IP addresses that are hammering your login page.
151
+ # After 20 requests in 1 minute, block all requests from that IP for 1 hour.
152
+ Rack::Attack.blacklist('allow2ban login scrapers') do |req|
153
+ # `filter` returns false value if request is to your login page (but still
154
+ # increments the count) so request below the limit are not blocked until
155
+ # they hit the limit. At that point, filter will return true and block.
156
+ Rack::Attack::Allow2Ban.filter(req.ip, :maxretry => 20, :findtime => 1.minute, :bantime => 1.hour) do
157
+ # The count for the IP is incremented if the return value is truthy.
158
+ req.path == '/login' and req.post?
159
+ end
160
+ end
161
+ ```
162
+
163
+
164
+ ### Throttles
165
+
166
+ ```ruby
167
+ # Throttle requests to 5 requests per second per ip
168
+ Rack::Attack.throttle('req/ip', :limit => 5, :period => 1.second) do |req|
169
+ # If the return value is truthy, the cache key for the return value
170
+ # is incremented and compared with the limit. In this case:
171
+ # "rack::attack:#{Time.now.to_i/1.second}:req/ip:#{req.ip}"
172
+ #
173
+ # If falsy, the cache key is neither incremented nor checked.
174
+
175
+ req.ip
176
+ end
177
+
178
+ # Throttle login attempts for a given email parameter to 6 reqs/minute
179
+ # Return the email as a discriminator on POST /login requests
180
+ Rack::Attack.throttle('logins/email', :limit => 6, :period => 60.seconds) do |req|
181
+ req.params['email'] if req.path == '/login' && req.post?
182
+ end
183
+
184
+ # You can also set a limit using a proc instead of a number. For
185
+ # instance, after Rack::Auth::Basic has authenticated the user:
186
+ limit_based_on_proc = proc {|req| req.env["REMOTE_USER"] == "admin" ? 100 : 1}
187
+ Rack::Attack.throttle('req/ip', :limit => limit_based_on_proc, :period => 1.second) do |req|
188
+ req.ip
189
+ end
190
+ ```
191
+
192
+ ### Conditional Throttles
193
+ # Throttle failed logins
194
+ Rack::Attack.conditional_throttle('login', :limit =>5, :period => 1.hour) do |req|
195
+ # If the return value is truthy, the cache key for the return value is checked to determine
196
+ # whether to throttle. The value for the key is not incremented
197
+
198
+ # To increment the value returned by key
199
+ To increment counter
200
+ if(failed_login)
201
+ Rack::Attack.increment_throttle_counter('login', request.params['...
202
+ end
203
+
204
+ ### Tracks
205
+
206
+ ```ruby
207
+ # Track requests from a special user agent.
208
+ Rack::Attack.track("special_agent") do |req|
209
+ req.user_agent == "SpecialAgent"
210
+ end
211
+
212
+ # Supports optional limit and period, triggers the notification only when the limit is reached.
213
+ Rack::Attack.track("special_agent", :limit 6, :period => 60.seconds) do |req|
214
+ req.user_agent == "SpecialAgent"
215
+ end
216
+
217
+ # Track it using ActiveSupport::Notification
218
+ ActiveSupport::Notifications.subscribe("rack.attack") do |name, start, finish, request_id, req|
219
+ if req.env['rack.attack.matched'] == "special_agent" && req.env['rack.attack.match_type'] == :track
220
+ Rails.logger.info "special_agent: #{req.path}"
221
+ STATSD.increment("special_agent")
222
+ end
223
+ end
224
+ ```
225
+
226
+ ## Responses
227
+
228
+ Customize the response of blacklisted and throttled requests using an object that adheres to the [Rack app interface](http://rack.rubyforge.org/doc/SPEC.html).
229
+
230
+ ```ruby
231
+ Rack::Attack.blacklisted_response = lambda do |env|
232
+ # Using 503 because it may make attacker think that they have successfully
233
+ # DOSed the site. Rack::Attack returns 403 for blacklists by default
234
+ [ 503, {}, ['Blocked']]
235
+ end
236
+
237
+ Rack::Attack.throttled_response = lambda do |env|
238
+ # name and other data about the matched throttle
239
+ body = [
240
+ env['rack.attack.matched'],
241
+ env['rack.attack.match_type'],
242
+ env['rack.attack.match_data']
243
+ ].inspect
244
+
245
+ # Using 503 because it may make attacker think that they have successfully
246
+ # DOSed the site. Rack::Attack returns 429 for throttling by default
247
+ [ 503, {}, [body]]
248
+ end
249
+ ```
250
+
251
+ For responses that did not exceed a throttle limit, Rack::Attack annotates the env with match data:
252
+
253
+ ```ruby
254
+ request.env['rack.attack.throttle_data'][name] # => { :count => n, :period => p, :limit => l }
255
+ ```
256
+
257
+ ## Logging & Instrumentation
258
+
259
+ Rack::Attack uses the [ActiveSupport::Notifications](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) API if available.
260
+
261
+ You can subscribe to 'rack.attack' events and log it, graph it, etc:
262
+
263
+ ```ruby
264
+ ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, request_id, req|
265
+ puts req.inspect
266
+ end
267
+ ```
268
+
269
+ ## Testing
270
+
271
+ A note on developing and testing apps using Rack::Attack - if you are using throttling in particular, you will
272
+ need to enable the cache in your development environment. See [Caching with Rails](http://guides.rubyonrails.org/caching_with_rails.html)
273
+ for more on how to do this.
274
+
275
+ ## Performance
276
+
277
+ The overhead of running Rack::Attack is typically negligible (a few milliseconds per request),
278
+ but it depends on how many checks you've configured, and how long they take.
279
+ Throttles usually require a network roundtrip to your cache server(s),
280
+ so try to keep the number of throttle checks per request low.
281
+
282
+ If a request is blacklisted or throttled, the response is a very simple Rack response.
283
+ A single typical ruby web server thread can block several hundred requests per second.
284
+
285
+ Rack::Attack complements tools like `iptables` and nginx's [limit_conn_zone module](http://nginx.org/en/docs/http/ngx_http_limit_conn_module.html#limit_conn_zone).
286
+
287
+ ## Motivation
288
+
289
+ Abusive clients range from malicious login crackers to naively-written scrapers.
290
+ They hinder the security, performance, & availability of web applications.
291
+
292
+ It is impractical if not impossible to block abusive clients completely.
293
+
294
+ Rack::Attack aims to let developers quickly mitigate abusive requests and rely
295
+ less on short-term, one-off hacks to block a particular attack.
296
+
297
+ ## Mailing list
298
+
299
+ New releases of Rack::Attack are announced on
300
+ <rack.attack.announce@librelist.com>. To subscribe, just send an email to
301
+ <rack.attack.announce@librelist.com>. See the
302
+ [archives](http://librelist.com/browser/rack.attack.announce/).
303
+
304
+ ## License
305
+
306
+ Copyright Kickstarter, Inc.
307
+
308
+ Released under an [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,18 @@
1
+ require "rubygems"
2
+ require "bundler/setup"
3
+ require 'rake/testtask'
4
+
5
+ namespace :test do
6
+ Rake::TestTask.new(:units) do |t|
7
+ t.pattern = "spec/*_spec.rb"
8
+ end
9
+
10
+ Rake::TestTask.new(:integration) do |t|
11
+ t.pattern = "spec/integration/*_spec.rb"
12
+ end
13
+ end
14
+
15
+ desc 'Run tests'
16
+ task :test => %w[test:units test:integration]
17
+
18
+ task :default => :test
@@ -0,0 +1,122 @@
1
+ require 'rack'
2
+ require 'forwardable'
3
+
4
+ class Rack::Attack
5
+ autoload :Cache, 'rack/attack/cache'
6
+ autoload :Check, 'rack/attack/check'
7
+ autoload :Throttle, 'rack/attack/throttle'
8
+ autoload :ConditionalThrottle, 'rack/attack/conditional_throttle'
9
+ autoload :Whitelist, 'rack/attack/whitelist'
10
+ autoload :Blacklist, 'rack/attack/blacklist'
11
+ autoload :Track, 'rack/attack/track'
12
+ autoload :StoreProxy, 'rack/attack/store_proxy'
13
+ autoload :DalliProxy, 'rack/attack/store_proxy/dalli_proxy'
14
+ autoload :RedisStoreProxy, 'rack/attack/store_proxy/redis_store_proxy'
15
+ autoload :Fail2Ban, 'rack/attack/fail2ban'
16
+ autoload :Allow2Ban, 'rack/attack/allow2ban'
17
+ autoload :Request, 'rack/attack/request'
18
+
19
+ class << self
20
+
21
+ attr_accessor :notifier, :blacklisted_response, :throttled_response
22
+
23
+ def increment_throttle_counter(name, discriminator)
24
+ self.throttles[name].increment_counter(discriminator)
25
+ end
26
+
27
+ def whitelist(name, &block)
28
+ self.whitelists[name] = Whitelist.new(name, block)
29
+ end
30
+
31
+ def blacklist(name, &block)
32
+ self.blacklists[name] = Blacklist.new(name, block)
33
+ end
34
+
35
+ def throttle(name, options, &block)
36
+ self.throttles[name] = Throttle.new(name, options, block)
37
+ end
38
+
39
+ def conditional_throttle(name, options, &block)
40
+ self.throttles[name] = ConditionalThrottle.new(name, options, block)
41
+ end
42
+
43
+ def track(name, options = {}, &block)
44
+ self.tracks[name] = Track.new(name, options, block)
45
+ end
46
+
47
+ def whitelists; @whitelists ||= {}; end
48
+ def blacklists; @blacklists ||= {}; end
49
+ def throttles; @throttles ||= {}; end
50
+ def tracks; @tracks ||= {}; end
51
+
52
+ def whitelisted?(req)
53
+ whitelists.any? do |name, whitelist|
54
+ whitelist[req]
55
+ end
56
+ end
57
+
58
+ def blacklisted?(req)
59
+ blacklists.any? do |name, blacklist|
60
+ blacklist[req]
61
+ end
62
+ end
63
+
64
+ def throttled?(req)
65
+ throttles.any? do |name, throttle|
66
+ throttle[req]
67
+ end
68
+ end
69
+
70
+ def tracked?(req)
71
+ tracks.each_value do |tracker|
72
+ tracker[req]
73
+ end
74
+ end
75
+
76
+ def instrument(req)
77
+ notifier.instrument('rack.attack', req) if notifier
78
+ end
79
+
80
+ def cache
81
+ @cache ||= Cache.new
82
+ end
83
+
84
+ def clear!
85
+ @whitelists, @blacklists, @throttles, @tracks = {}, {}, {}, {}
86
+ end
87
+
88
+ end
89
+
90
+ # Set defaults
91
+ @notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
92
+ @blacklisted_response = lambda {|env| [403, {'Content-Type' => 'text/plain'}, ["Forbidden\n"]] }
93
+ @throttled_response = lambda {|env|
94
+ retry_after = env['rack.attack.match_data'][:period] rescue nil
95
+ [429, {'Content-Type' => 'text/plain', 'Retry-After' => retry_after.to_s}, ["Retry later\n"]]
96
+ }
97
+
98
+ def initialize(app)
99
+ @app = app
100
+ end
101
+
102
+ def call(env)
103
+ req = Rack::Attack::Request.new(env)
104
+
105
+ if whitelisted?(req)
106
+ @app.call(env)
107
+ elsif blacklisted?(req)
108
+ self.class.blacklisted_response[env]
109
+ elsif throttled?(req)
110
+ self.class.throttled_response[env]
111
+ else
112
+ tracked?(req)
113
+ @app.call(env)
114
+ end
115
+ end
116
+
117
+ extend Forwardable
118
+ def_delegators self, :whitelisted?,
119
+ :blacklisted?,
120
+ :throttled?,
121
+ :tracked?
122
+ end