suggesting changing whitelist/blacklist language to less controversial safelist/blocklist language

add deprication warnings

fix the method signatures
This commit is contained in:
Renée Hendricksen 2016-07-01 12:10:15 -04:00
parent e20c628460
commit e1a0c804e1
13 changed files with 121 additions and 75 deletions

2
.gitignore vendored
View file

@ -3,3 +3,5 @@ bin
.bundle
*.gem
*.gemfile.lock
.ruby-version
.ruby-gemset

View file

@ -27,7 +27,7 @@
## 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.
@ -35,7 +35,7 @@
## 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
@ -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.
@ -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

View file

@ -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)).
@ -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.
@ -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
@ -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
@ -147,7 +147,7 @@ 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
@ -155,7 +155,7 @@ clients until such time as they reach maxretry at which they are cut off as per
```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.
@ -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
@ -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).

View file

@ -15,12 +15,12 @@ Rack::Attack.throttle "logins/email", :limit => 2, :period => 60 do |req|
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

View file

@ -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'
@ -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)
@ -37,23 +47,43 @@ class Rack::Attack
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]
@ -75,14 +105,14 @@ class Rack::Attack
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"]]
@ -96,10 +126,10 @@ class Rack::Attack
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
@ -109,8 +139,8 @@ class Rack::Attack
end
extend Forwardable
def_delegators self, :whitelisted?,
:blacklisted?,
def_delegators self, :safelisted?,
:blocklisted?,
:throttled?,
:tracked?
end

View file

@ -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

View file

@ -8,7 +8,7 @@ module Rack
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)

View file

@ -9,7 +9,7 @@
# end
# end
#
# Rack::Attack.whitelist("localhost") {|req| req.localhost? }
# Rack::Attack.safelist("localhost") {|req| req.localhost? }
#
module Rack
class Attack

View file

@ -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

View file

@ -7,7 +7,7 @@ describe 'Rack::Attack.Allow2Ban' do
@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

View file

@ -7,7 +7,7 @@ describe 'Rack::Attack.Fail2Ban' do
@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

View file

@ -9,7 +9,7 @@ describe 'Rack::Attack' do
end
end
Rack::Attack.whitelist('valid IP') do |req|
Rack::Attack.safelist('valid IP') do |req|
req.remote_ip == "127.0.0.1"
end
end

View file

@ -5,7 +5,7 @@ describe 'Rack::Attack' do
describe 'normalizing paths' do
before do
Rack::Attack.blacklist("banned_path") {|req| req.path == '/foo' }
Rack::Attack.blocklist("banned_path") {|req| req.path == '/foo' }
end
it 'blocks requests with trailing slash' do
@ -14,47 +14,62 @@ describe 'Rack::Attack' do
end
end
describe 'blacklist' do
describe 'blocklist' do
before do
@bad_ip = '1.2.3.4'
Rack::Attack.blacklist("ip #{@bad_ip}") {|req| req.ip == @bad_ip }
Rack::Attack.blocklist("ip #{@bad_ip}") {|req| req.ip == @bad_ip }
end
it('has a blacklist') {
Rack::Attack.blacklists.key?("ip #{@bad_ip}").must_equal true
it('has a blocklist') {
Rack::Attack.blocklists.key?("ip #{@bad_ip}").must_equal true
}
it('has a blacklist with a deprication warning') {
stdout, stderror = capture_io do
Rack::Attack.blacklists.key?("ip #{@bad_ip}").must_equal true
end
assert_match "[DEPRECATION] 'blacklists' is deprecated. Please use 'blocklists' instead.", stderror
}
describe "a bad request" do
before { get '/', {}, 'REMOTE_ADDR' => @bad_ip }
it "should return a blacklist response" do
it "should return a blocklist response" do
get '/', {}, 'REMOTE_ADDR' => @bad_ip
last_response.status.must_equal 403
last_response.body.must_equal "Forbidden\n"
end
it "should tag the env" do
last_request.env['rack.attack.matched'].must_equal "ip #{@bad_ip}"
last_request.env['rack.attack.match_type'].must_equal :blacklist
last_request.env['rack.attack.match_type'].must_equal :blocklist
end
allow_ok_requests
end
describe "and whitelist" do
describe "and safelist" do
before do
@good_ua = 'GoodUA'
Rack::Attack.whitelist("good ua") {|req| req.user_agent == @good_ua }
Rack::Attack.safelist("good ua") {|req| req.user_agent == @good_ua }
end
it('has a whitelist'){ Rack::Attack.whitelists.key?("good ua") }
describe "with a request match both whitelist & blacklist" do
it('has a safelist'){ Rack::Attack.safelists.key?("good ua") }
it('has a whitelist with a deprication warning') {
stdout, stderror = capture_io do
Rack::Attack.whitelists.key?("good ua")
end
assert_match "[DEPRECATION] 'whitelists' is deprecated. Please use 'safelists' instead.", stderror
}
describe "with a request match both safelist & blocklist" do
before { get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua }
it "should allow whitelists before blacklists" do
it "should allow safelists before blocklists" do
get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua
last_response.status.must_equal 200
end
it "should tag the env" do
last_request.env['rack.attack.matched'].must_equal 'good ua'
last_request.env['rack.attack.match_type'].must_equal :whitelist
last_request.env['rack.attack.match_type'].must_equal :safelist
end
end
end