rack-attack 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rack-attack might be problematic. Click here for more details.
- data/README.md +119 -6
- data/lib/rack/attack.rb +19 -19
- data/lib/rack/attack/check.rb +4 -1
- data/lib/rack/attack/throttle.rb +6 -3
- data/lib/rack/attack/version.rb +1 -1
- data/spec/rack_attack_spec.rb +38 -18
- metadata +3 -3
data/README.md
CHANGED
@@ -1,9 +1,122 @@
|
|
1
|
-
# Rack::Attack
|
1
|
+
# Rack::Attack
|
2
|
+
A DSL for blocking & thottling abusive clients
|
2
3
|
|
3
|
-
|
4
|
-
*
|
5
|
-
* If any blacklist matches, the request is blocked (unless a whitelist matched)
|
6
|
-
* If any throttle matches, the request is throttled (unless a whitelist or blacklist matched)
|
4
|
+
Rack::Attack is a rack middleware to protect your web app from bad clients.
|
5
|
+
It allows *whitelisting*, *blacklisting*, and *thottling* based on arbitrary properties of the request.
|
7
6
|
|
8
|
-
|
7
|
+
Thottle state is stored in a configurable cache (e.g. `Rails.cache`), presumably backed by memcached.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add the [rack-attack](http://rubygems.org/gems/rack-attack) gem to your Gemfile or run
|
12
|
+
|
13
|
+
gem install rack-attack
|
14
|
+
|
15
|
+
Tell your app to use the Rack::Attack middleware.
|
16
|
+
For Rails 3 apps:
|
17
|
+
|
18
|
+
# In config/application.rb
|
19
|
+
config.middleware.use Rack::Attack
|
20
|
+
|
21
|
+
Or in your `config.ru`:
|
22
|
+
|
23
|
+
use Rack::Attack
|
24
|
+
|
25
|
+
Optionally configure the cache store for throttling:
|
26
|
+
|
27
|
+
Rack::Attack.cache.store = my_cache_store # defaults to Rails.cache
|
28
|
+
|
29
|
+
Note that `Rack::Attack.cache` is only used for throttling, not blacklisting & whitelisting.
|
30
|
+
|
31
|
+
## How it works
|
32
|
+
|
33
|
+
The Rack::Attack middleware examines each request against *whitelists*, *blacklists*, and *throttles* that you define. There are none by default.
|
34
|
+
|
35
|
+
* If the request matches any whitelist, the request is allowed. Blacklists and throttles are not checked.
|
36
|
+
* If the request matches any blacklist, the request is blocked. Throttles are not checked.
|
37
|
+
* If the request matches any throttle, a counter is incremented in the Rack::Attack.cache. If the throttle limit is exceeded, the request is blocked and further throttles are not checked.
|
38
|
+
|
39
|
+
## Usage
|
40
|
+
|
41
|
+
Define blacklists, throttles, and whitelists.
|
42
|
+
Note that `req` is a [Rack::Request](http://rack.rubyforge.org/doc/classes/Rack/Request.html) object.
|
43
|
+
|
44
|
+
### Blacklists
|
45
|
+
|
46
|
+
# Block requests from 1.2.3.4
|
47
|
+
Rack::Attack.blacklist('block 1.2.3.4') do |req|
|
48
|
+
# Request are blocked if the return value is truthy
|
49
|
+
'1.2.3.4' == req.ip
|
50
|
+
end
|
51
|
+
|
52
|
+
# Block logins from a bad user agent
|
53
|
+
Rack::Attack.blacklist('block bad UA logins') do |req|
|
54
|
+
req.post? && request.path == '/login' && req.user_agent == 'BadUA'
|
55
|
+
end
|
56
|
+
|
57
|
+
### Throttles
|
58
|
+
|
59
|
+
# Throttle requests to 5 requests per second per ip
|
60
|
+
Rack::Attack.throttle('req/ip', :limit => 5, :period => 1.second) do |req|
|
61
|
+
# If the return value is truthy, the cache key for "rack::attack:req/ip:#{req.ip}" is incremented and checked.
|
62
|
+
# If falsy, the cache key is neither incremented or checked.
|
63
|
+
req.ip
|
64
|
+
end
|
9
65
|
|
66
|
+
# Throttle login attempts for a given email parameter to 6 reqs/minute
|
67
|
+
Rack::Attack.throttle('logins/email', :limit => 6, :period => 60.seconds) do |req|
|
68
|
+
req.post? && request.path == '/login' && req.params['email']
|
69
|
+
end
|
70
|
+
|
71
|
+
### Whitelists
|
72
|
+
|
73
|
+
# Always allow requests from localhost
|
74
|
+
# (blacklist & throttles are skipped)
|
75
|
+
Rack::Attack.whitelist('allow from localhost') do |req|
|
76
|
+
# Requests are allowed if the return value is truthy
|
77
|
+
'127.0.0.1' == req.ip
|
78
|
+
end
|
79
|
+
|
80
|
+
## Responses
|
81
|
+
|
82
|
+
Customize the response of throttled requests using an object that adheres to the [Rack app interface](http://rack.rubyforge.org/doc/SPEC.html).
|
83
|
+
|
84
|
+
Rack:Attack.throttled_response = lambda do |env|
|
85
|
+
env['rack.attack.throttled'] # name and other data about the matched throttle
|
86
|
+
[ 503, {}, ['Throttled']]
|
87
|
+
end
|
88
|
+
|
89
|
+
Similarly for blacklisted responses:
|
90
|
+
|
91
|
+
Rack:Attack.blacklisted_response = lambda do |env|
|
92
|
+
env['rack.attack.blacklisted'] # name of the matched blacklist
|
93
|
+
[ 503, {}, ['Blocked']]
|
94
|
+
end
|
95
|
+
|
96
|
+
## Logging & Instrumentation
|
97
|
+
|
98
|
+
Rack::Attack uses the [ActiveSupport::Notifications](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) API if available.
|
99
|
+
|
100
|
+
You can subscribe to 'rack.attack.{blacklist,throttle,whitelist}' events and log it, graph it, etc:
|
101
|
+
|
102
|
+
ActiveSupport::Notifications.subscribe('rack.attack.blacklist') do |name, start, finish, request_id, req|
|
103
|
+
puts req.inspect
|
104
|
+
end
|
105
|
+
|
106
|
+
## Motivation
|
107
|
+
|
108
|
+
Abusive clients range from malicious login crackers to naively-written scrapers.
|
109
|
+
They hinder the security, performance, & availability of web applications.
|
110
|
+
|
111
|
+
It is impractical if not impossible to block abusive clients completely.
|
112
|
+
|
113
|
+
Rack::Attack aims to let developers quickly mitigate abusive requests and rely
|
114
|
+
less on short-term, one-off hacks to block a particular attack.
|
115
|
+
|
116
|
+
Rack::Attack complements `iptables` and nginx's [limit_zone module](http://wiki.nginx.org/HttpLimitZoneModule).
|
117
|
+
|
118
|
+
## Thanks
|
119
|
+
|
120
|
+
Thanks to [Kickstarter](https://github.com/kickstarter) for sponsoring Rack::Attack development
|
121
|
+
|
122
|
+
[![Travis CI](https://secure.travis-ci.org/ktheory/rack-attack.png)](http://travis-ci.org/ktheory/rack-attack)
|
data/lib/rack/attack.rb
CHANGED
@@ -8,17 +8,18 @@ module Rack::Attack
|
|
8
8
|
class << self
|
9
9
|
|
10
10
|
attr_reader :cache, :notifier
|
11
|
+
attr_accessor :blacklisted_response, :throttled_response
|
11
12
|
|
12
13
|
def whitelist(name, &block)
|
13
|
-
|
14
|
+
self.whitelists[name] = Whitelist.new(name, block)
|
14
15
|
end
|
15
16
|
|
16
17
|
def blacklist(name, &block)
|
17
|
-
|
18
|
+
self.blacklists[name] = Blacklist.new(name, block)
|
18
19
|
end
|
19
20
|
|
20
21
|
def throttle(name, options, &block)
|
21
|
-
|
22
|
+
self.throttles[name] = Throttle.new(name, options, block)
|
22
23
|
end
|
23
24
|
|
24
25
|
def whitelists; @whitelists ||= {}; end
|
@@ -26,13 +27,20 @@ module Rack::Attack
|
|
26
27
|
def throttles; @throttles ||= {}; end
|
27
28
|
|
28
29
|
def new(app)
|
29
|
-
@cache ||= Cache.new
|
30
|
-
@notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
|
31
30
|
@app = app
|
31
|
+
|
32
|
+
# Set defaults
|
33
|
+
@cache ||= Cache.new
|
34
|
+
@notifier ||= ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
|
35
|
+
@blacklisted_response ||= lambda {|env| [503, {}, ['Blocked']] }
|
36
|
+
@throttled_response ||= lambda {|env|
|
37
|
+
retry_after = env['rack.attack.matched'][:period] rescue nil
|
38
|
+
[503, {'Retry-After' => retry_after}, ['Retry later']]
|
39
|
+
}
|
40
|
+
|
32
41
|
self
|
33
42
|
end
|
34
43
|
|
35
|
-
|
36
44
|
def call(env)
|
37
45
|
req = Rack::Request.new(env)
|
38
46
|
|
@@ -41,9 +49,9 @@ module Rack::Attack
|
|
41
49
|
end
|
42
50
|
|
43
51
|
if blacklisted?(req)
|
44
|
-
blacklisted_response
|
52
|
+
blacklisted_response[env]
|
45
53
|
elsif throttled?(req)
|
46
|
-
throttled_response
|
54
|
+
throttled_response[env]
|
47
55
|
else
|
48
56
|
@app.call(env)
|
49
57
|
end
|
@@ -67,19 +75,11 @@ module Rack::Attack
|
|
67
75
|
end
|
68
76
|
end
|
69
77
|
|
70
|
-
def instrument(payload)
|
71
|
-
notifier.instrument(
|
72
|
-
end
|
73
|
-
|
74
|
-
def blacklisted_response
|
75
|
-
[503, {}, ['Blocked']]
|
76
|
-
end
|
77
|
-
|
78
|
-
def throttled_response
|
79
|
-
[503, {}, ['Throttled']]
|
78
|
+
def instrument(type, payload)
|
79
|
+
notifier.instrument("rack.attack.#{type}", payload) if notifier
|
80
80
|
end
|
81
81
|
|
82
|
-
|
82
|
+
def clear!
|
83
83
|
@whitelists, @blacklists, @throttles = {}, {}, {}
|
84
84
|
end
|
85
85
|
|
data/lib/rack/attack/check.rb
CHANGED
@@ -9,7 +9,10 @@ module Rack
|
|
9
9
|
|
10
10
|
def [](req)
|
11
11
|
block[req].tap {|match|
|
12
|
-
|
12
|
+
if match
|
13
|
+
req.env["rack.attack.matched"] = {type => name}
|
14
|
+
Rack::Attack.instrument(type, req)
|
15
|
+
end
|
13
16
|
}
|
14
17
|
end
|
15
18
|
|
data/lib/rack/attack/throttle.rb
CHANGED
@@ -21,9 +21,12 @@ module Rack
|
|
21
21
|
|
22
22
|
key = "#{name}:#{discriminator}"
|
23
23
|
count = cache.count(key, period)
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
(count > limit).tap do |throttled|
|
25
|
+
if throttled
|
26
|
+
req.env['rack.attack.matched'] = {:throttle => name, :count => count, :period => period, :limit => limit}
|
27
|
+
Rack::Attack.instrument(:throttle, req)
|
28
|
+
end
|
29
|
+
end
|
27
30
|
end
|
28
31
|
|
29
32
|
end
|
data/lib/rack/attack/version.rb
CHANGED
data/spec/rack_attack_spec.rb
CHANGED
@@ -30,12 +30,18 @@ describe 'Rack::Attack' do
|
|
30
30
|
|
31
31
|
it('has a blacklist') { Rack::Attack.blacklists.key?("ip #{@bad_ip}") }
|
32
32
|
|
33
|
-
|
34
|
-
get '/', {}, 'REMOTE_ADDR' => @bad_ip
|
35
|
-
|
36
|
-
|
33
|
+
describe "a bad request" do
|
34
|
+
before { get '/', {}, 'REMOTE_ADDR' => @bad_ip }
|
35
|
+
it "should return a blacklist response" do
|
36
|
+
get '/', {}, 'REMOTE_ADDR' => @bad_ip
|
37
|
+
last_response.status.must_equal 503
|
38
|
+
end
|
39
|
+
it "should tag the env" do
|
40
|
+
last_request.env['rack.attack.matched'].must_equal({:blacklist => "ip #{@bad_ip}"})
|
41
|
+
end
|
37
42
|
|
38
|
-
|
43
|
+
allow_ok_requests
|
44
|
+
end
|
39
45
|
|
40
46
|
describe "and with a whitelist" do
|
41
47
|
before do
|
@@ -44,9 +50,15 @@ describe 'Rack::Attack' do
|
|
44
50
|
end
|
45
51
|
|
46
52
|
it('has a whitelist'){ Rack::Attack.whitelists.key?("good ua") }
|
47
|
-
|
48
|
-
get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua
|
49
|
-
|
53
|
+
describe "with a request match both whitelist & blacklist" do
|
54
|
+
before { get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua }
|
55
|
+
it "should allow whitelists before blacklists" do
|
56
|
+
get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua
|
57
|
+
last_response.status.must_equal 200
|
58
|
+
end
|
59
|
+
it "should tag the env" do
|
60
|
+
last_request.env['rack.attack.matched'].must_equal({:whitelist => 'good ua'})
|
61
|
+
end
|
50
62
|
end
|
51
63
|
end
|
52
64
|
end
|
@@ -60,18 +72,26 @@ describe 'Rack::Attack' do
|
|
60
72
|
it('should have a throttle'){ Rack::Attack.throttles.key?('ip/sec') }
|
61
73
|
allow_ok_requests
|
62
74
|
|
63
|
-
|
64
|
-
get '/', {}, 'REMOTE_ADDR' => '1.2.3.4'
|
65
|
-
|
75
|
+
describe 'a single request' do
|
76
|
+
before { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
|
77
|
+
it 'should set the counter for one request' do
|
78
|
+
Rack::Attack.cache.store.read('rack::attack:ip/sec:1.2.3.4').must_equal 1
|
79
|
+
end
|
66
80
|
end
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
81
|
+
describe "with 2 requests" do
|
82
|
+
before do
|
83
|
+
2.times { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
|
84
|
+
end
|
85
|
+
it 'should block the last request' do
|
86
|
+
last_response.status.must_equal 503
|
87
|
+
end
|
88
|
+
it 'should tag the env' do
|
89
|
+
last_request.env['rack.attack.matched'].must_equal({:throttle => 'ip/sec', :count => 2, :limit => 1, :period => 1})
|
90
|
+
end
|
91
|
+
it 'should set a Retry-After header' do
|
92
|
+
last_response.headers['Retry-After'].must_equal 1
|
71
93
|
end
|
72
|
-
last_response.status.must_equal 503
|
73
94
|
end
|
74
|
-
end
|
75
|
-
|
76
95
|
|
96
|
+
end
|
77
97
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-attack
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-07-
|
12
|
+
date: 2012-07-30 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rack
|
@@ -107,7 +107,7 @@ dependencies:
|
|
107
107
|
- - ~>
|
108
108
|
- !ruby/object:Gem::Version
|
109
109
|
version: 1.1.3
|
110
|
-
description: A
|
110
|
+
description: A rack middleware for throttling and blocking abusive requests
|
111
111
|
email: aaron@ktheory.com
|
112
112
|
executables: []
|
113
113
|
extensions: []
|