Skip to content

Commit

Permalink
suggesting changing whitelist/blacklist language to less controversia…
Browse files Browse the repository at this point in the history
…l safelist/blocklist language

add deprication warnings

fix the method signatures
  • Loading branch information
renee-travisci committed Jul 2, 2016
1 parent e20c628 commit e1a0c80
Show file tree
Hide file tree
Showing 13 changed files with 121 additions and 75 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ bin
.bundle
*.gem
*.gemfile.lock
.ruby-version
.ruby-gemset
8 changes: 4 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@

## v4.2.0 - 26 Oct 2014
- Throttle's `period` argument now takes a proc as well as a number (thanks @gsamokovarov)
- Invoke the `#call` method on `blacklist_response` and `throttle_response` instead of `#[]`, as per the Rack spec. (thanks @gsamokovarov)
- Invoke the `#call` method on `blocklist_response` and `throttle_response` instead of `#[]`, as per the Rack spec. (thanks @gsamokovarov)

## v4.1.1 - 11 Sept 2014
- Fix a race condition in throttles that could allow more requests than intended.

## v4.1.0 - 22 May 2014
- Tracks take an optional limit and period to only notify once a threshold
is reached (similar to throttles). Thanks @chiliburger!
- Default throttled & blacklist responses have Content-Type: text/plain
- Default throttled & blocklist responses have Content-Type: text/plain
- Rack::Attack.clear! resets tracks

## v4.0.1 - 14 May 2014
Expand All @@ -49,7 +49,7 @@
* Test more dalli versions.

## v3.0.0 - 15 March 2014
* Change default blacklisted response to 403 Forbidden (thanks @carpodaster).
* Change default blocklisted response to 403 Forbidden (thanks @carpodaster).
* Fail gracefully when Redis store is not available; rescue exeption and don't
throttle request. (thanks @wkimeria)
* TravisCI runs integration tests.
Expand All @@ -62,7 +62,7 @@
## v2.2.1 - 13 August 2013
* Add license to gemspec
* Support ruby version 1.9.2
* Change default blacklisted response code from 503 to 401; throttled response
* Change default blocklisted response code from 503 to 401; throttled response
from 503 to 429.

## v2.2.0 - 20 June 2013
Expand Down
48 changes: 24 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
*Rack middleware for blocking & throttling abusive requests*

Rack::Attack is a rack middleware to protect your web app from bad clients.
It allows *whitelisting*, *blacklisting*, *throttling*, and *tracking* based on arbitrary properties of the request.
It allows *safelisting*, *blocklisting*, *throttling*, and *tracking* based on arbitrary properties of the request.

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)).

Expand Down Expand Up @@ -53,14 +53,14 @@ Optionally configure the cache store for throttling or fail2ban filtering:
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new # defaults to Rails.cache
```

Note that `Rack::Attack.cache` is only used for throttling and fail2ban filtering; not blacklisting & whitelisting. Your cache store must implement `increment` and `write` like [ActiveSupport::Cache::Store](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html).
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).

## How it works

The Rack::Attack middleware compares each request against *whitelists*, *blacklists*, *throttles*, and *tracks* that you define. There are none by default.
The Rack::Attack middleware compares each request against *safelists*, *blocklists*, *throttles*, and *tracks* that you define. There are none by default.

* If the request matches any **whitelist**, it is allowed.
* Otherwise, if the request matches any **blacklist**, it is blocked.
* If the request matches any **safelist**, it is allowed.
* Otherwise, if the request matches any **blocklist**, it is blocked.
* 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.
* Otherwise, all **tracks** are checked, and the request is allowed.

Expand All @@ -70,10 +70,10 @@ The algorithm is actually more concise in code: See [Rack::Attack.call](https://
def call(env)
req = Rack::Attack::Request.new(env)

if whitelisted?(req)
if safelisted?(req)
@app.call(env)
elsif blacklisted?(req)
self.class.blacklisted_response.call(env)
elsif blocklisted?(req)
self.class.blocklisted_response.call(env)
elsif throttled?(req)
self.class.throttled_response.call(env)
else
Expand All @@ -93,47 +93,47 @@ can cleanly monkey patch helper methods onto the

## Usage

Define whitelists, blacklists, throttles, and tracks as blocks that return truthy values if matched, falsy otherwise. In a Rails app
Define safelists, blocklists, throttles, and tracks as blocks that return truthy values if matched, falsy otherwise. In a Rails app
these go in an initializer in `config/initializers/`.
A [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request) object is passed to the block (named 'req' in the examples).

### Whitelists
### safelists

```ruby
# Always allow requests from localhost
# (blacklist & throttles are skipped)
Rack::Attack.whitelist('allow from localhost') do |req|
# (blocklist & throttles are skipped)
Rack::Attack.safelist('allow from localhost') do |req|
# Requests are allowed if the return value is truthy
'127.0.0.1' == req.ip || '::1' == req.ip
end
```

### Blacklists
### blocklists

```ruby
# Block requests from 1.2.3.4
Rack::Attack.blacklist('block 1.2.3.4') do |req|
Rack::Attack.blocklist('block 1.2.3.4') do |req|
# Requests are blocked if the return value is truthy
'1.2.3.4' == req.ip
end

# Block logins from a bad user agent
Rack::Attack.blacklist('block bad UA logins') do |req|
Rack::Attack.blocklist('block bad UA logins') do |req|
req.path == '/login' && req.post? && req.user_agent == 'BadUA'
end
```

#### Fail2Ban

`Fail2Ban.filter` can be used within a blacklist to block all requests from misbehaving clients.
`Fail2Ban.filter` can be used within a blocklist to block all requests from misbehaving clients.
This pattern is inspired by [fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page).
See the [fail2ban documentation](http://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jail_Options) for more details on
how the parameters work. For multiple filters, be sure to put each filter in a separate blacklist and use a unique discriminator for each fail2ban filter.
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.

```ruby
# Block suspicious requests for '/etc/password' or wordpress specific paths.
# After 3 blocked requests in 10 minutes, block all requests from that IP for 5 minutes.
Rack::Attack.blacklist('fail2ban pentesters') do |req|
Rack::Attack.blocklist('fail2ban pentesters') do |req|
# `filter` returns truthy value if request fails, or if it's from a previously banned IP
# so the request is blocked
Rack::Attack::Fail2Ban.filter("pentesters-#{req.ip}", :maxretry => 3, :findtime => 10.minutes, :bantime => 5.minutes) do
Expand All @@ -147,15 +147,15 @@ Rack::Attack.blacklist('fail2ban pentesters') do |req|
end
```

Note that `Fail2Ban` filters are not automatically scoped to the blacklist, so when using multiple filters in an application the scoping must be added to the discriminator e.g. `"pentest:#{req.ip}"`.
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}"`.

#### Allow2Ban
`Allow2Ban.filter` works the same way as the `Fail2Ban.filter` except that it *allows* requests from misbehaving
clients until such time as they reach maxretry at which they are cut off as per normal.
```ruby
# Lockout IP addresses that are hammering your login page.
# After 20 requests in 1 minute, block all requests from that IP for 1 hour.
Rack::Attack.blacklist('allow2ban login scrapers') do |req|
Rack::Attack.blocklist('allow2ban login scrapers') do |req|
# `filter` returns false value if request is to your login page (but still
# increments the count) so request below the limit are not blocked until
# they hit the limit. At that point, filter will return true and block.
Expand Down Expand Up @@ -220,12 +220,12 @@ end

## Responses

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).
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).

```ruby
Rack::Attack.blacklisted_response = lambda do |env|
Rack::Attack.blocklisted_response = lambda do |env|
# Using 503 because it may make attacker think that they have successfully
# DOSed the site. Rack::Attack returns 403 for blacklists by default
# DOSed the site. Rack::Attack returns 403 for blocklists by default
[ 503, {}, ['Blocked']]
end

Expand Down Expand Up @@ -274,7 +274,7 @@ but it depends on how many checks you've configured, and how long they take.
Throttles usually require a network roundtrip to your cache server(s),
so try to keep the number of throttle checks per request low.

If a request is blacklisted or throttled, the response is a very simple Rack response.
If a request is blocklisted or throttled, the response is a very simple Rack response.
A single typical ruby web server thread can block several hundred requests per second.

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).
Expand Down
8 changes: 4 additions & 4 deletions examples/rack_attack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
req.post? && req.path == "/login" && req.params['email']
end

# Blacklist bad IPs from accessing admin pages
Rack::Attack.blacklist "bad_ips from logging in" do |req|
# blocklist bad IPs from accessing admin pages
Rack::Attack.blocklist "bad_ips from logging in" do |req|
req.path =~ /^\/admin/ && bad_ips.include?(req.ip)
end

# Whitelist a User-Agent
Rack::Attack.whitelist 'internal user agent' do |req|
# safelist a User-Agent
Rack::Attack.safelist 'internal user agent' do |req|
req.user_agent == 'InternalUserAgent'
end
70 changes: 50 additions & 20 deletions lib/rack/attack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ class Rack::Attack
autoload :PathNormalizer, 'rack/attack/path_normalizer'
autoload :Check, 'rack/attack/check'
autoload :Throttle, 'rack/attack/throttle'
autoload :Whitelist, 'rack/attack/whitelist'
autoload :Blacklist, 'rack/attack/blacklist'
autoload :Safelist, 'rack/attack/safelist'
autoload :Blocklist, 'rack/attack/blocklist'
autoload :Track, 'rack/attack/track'
autoload :StoreProxy, 'rack/attack/store_proxy'
autoload :DalliProxy, 'rack/attack/store_proxy/dalli_proxy'
Expand All @@ -19,14 +19,24 @@ class Rack::Attack

class << self

attr_accessor :notifier, :blacklisted_response, :throttled_response
attr_accessor :notifier, :blocklisted_response, :throttled_response

def safelist(name, &block)
self.safelists[name] = Safelist.new(name, block)
end

def whitelist(name, &block)
self.whitelists[name] = Whitelist.new(name, block)
warn "[DEPRECATION] 'whitelist' is deprecated. Please use 'safelist' instead."
safelist(name, &block)
end

def blocklist(name, &block)
self.blocklists[name] = Blocklist.new(name, block)
end

def blacklist(name, &block)
self.blacklists[name] = Blacklist.new(name, block)
warn "[DEPRECATION] 'blacklist' is deprecated. Please use 'blocklist' instead."
blocklist(name, &block)
end

def throttle(name, options, &block)
Expand All @@ -37,23 +47,43 @@ def track(name, options = {}, &block)
self.tracks[name] = Track.new(name, options, block)
end

def whitelists; @whitelists ||= {}; end
def blacklists; @blacklists ||= {}; end
def safelists; @safelists ||= {}; end
def blocklists; @blocklists ||= {}; end
def throttles; @throttles ||= {}; end
def tracks; @tracks ||= {}; end

def whitelisted?(req)
whitelists.any? do |name, whitelist|
whitelist[req]
def whitelists
warn "[DEPRECATION] 'whitelists' is deprecated. Please use 'safelists' instead."
safelists
end

def blacklists
warn "[DEPRECATION] 'blacklists' is deprecated. Please use 'blocklists' instead."
blocklists
end

def safelisted?(req)
safelists.any? do |name, safelist|
safelist[req]
end
end

def blacklisted?(req)
blacklists.any? do |name, blacklist|
blacklist[req]
def whitelisted?
warn "[DEPRECATION] 'whitelisted?' is deprecated. Please use 'safelisted?' instead."
safelisted?
end

def blocklisted?(req)
blocklists.any? do |name, blocklist|
blocklist[req]
end
end

def blacklisted?
warn "[DEPRECATION] 'blacklisted?' is deprecated. Please use 'blocklisted?' instead."
blocklisted?
end

def throttled?(req)
throttles.any? do |name, throttle|
throttle[req]
Expand All @@ -75,14 +105,14 @@ def cache
end

def clear!
@whitelists, @blacklists, @throttles, @tracks = {}, {}, {}, {}
@safelists, @blocklists, @throttles, @tracks = {}, {}, {}, {}
end

end

# Set defaults
@notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
@blacklisted_response = lambda {|env| [403, {'Content-Type' => 'text/plain'}, ["Forbidden\n"]] }
@blocklisted_response = lambda {|env| [403, {'Content-Type' => 'text/plain'}, ["Forbidden\n"]] }
@throttled_response = lambda {|env|
retry_after = (env['rack.attack.match_data'] || {})[:period]
[429, {'Content-Type' => 'text/plain', 'Retry-After' => retry_after.to_s}, ["Retry later\n"]]
Expand All @@ -96,10 +126,10 @@ def call(env)
env['PATH_INFO'] = PathNormalizer.normalize_path(env['PATH_INFO'])
req = Rack::Attack::Request.new(env)

if whitelisted?(req)
if safelisted?(req)
@app.call(env)
elsif blacklisted?(req)
self.class.blacklisted_response.call(env)
elsif blocklisted?(req)
self.class.blocklisted_response.call(env)
elsif throttled?(req)
self.class.throttled_response.call(env)
else
Expand All @@ -109,8 +139,8 @@ def call(env)
end

extend Forwardable
def_delegators self, :whitelisted?,
:blacklisted?,
def_delegators self, :safelisted?,
:blocklisted?,
:throttled?,
:tracked?
end
4 changes: 2 additions & 2 deletions lib/rack/attack/whitelist.rb → lib/rack/attack/blocklist.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
module Rack
class Attack
class Whitelist < Check
class Blocklist < Check
def initialize(name, block)
super
@type = :whitelist
@type = :blocklist
end

end
Expand Down
2 changes: 1 addition & 1 deletion lib/rack/attack/fail2ban.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def filter(discriminator, options)
maxretry = options[:maxretry] or raise ArgumentError, "Must pass maxretry option"

if banned?(discriminator)
# Return true for blacklist
# Return true for blocklist
true
elsif yield
fail!(discriminator, bantime, findtime, maxretry)
Expand Down
2 changes: 1 addition & 1 deletion lib/rack/attack/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# end
# end
#
# Rack::Attack.whitelist("localhost") {|req| req.localhost? }
# Rack::Attack.safelist("localhost") {|req| req.localhost? }
#
module Rack
class Attack
Expand Down
5 changes: 2 additions & 3 deletions lib/rack/attack/blacklist.rb → lib/rack/attack/safelist.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
module Rack
class Attack
class Blacklist < Check
class Safelist < Check
def initialize(name, block)
super
@type = :blacklist
@type = :safelist
end

end
end
end

2 changes: 1 addition & 1 deletion spec/allow2ban_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
@bantime = 60
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
@f2b_options = {:bantime => @bantime, :findtime => @findtime, :maxretry => 2}
Rack::Attack.blacklist('pentest') do |req|
Rack::Attack.blocklist('pentest') do |req|
Rack::Attack::Allow2Ban.filter(req.ip, @f2b_options){req.query_string =~ /OMGHAX/}
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/fail2ban_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
@bantime = 60
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
@f2b_options = {:bantime => @bantime, :findtime => @findtime, :maxretry => 2}
Rack::Attack.blacklist('pentest') do |req|
Rack::Attack.blocklist('pentest') do |req|
Rack::Attack::Fail2Ban.filter(req.ip, @f2b_options){req.query_string =~ /OMGHAX/}
end
end
Expand Down
Loading

0 comments on commit e1a0c80

Please sign in to comment.