rack-attack 0.0.3 → 0.1.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.
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
|
+
[](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: []
|