Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Controversial Language Change Suggestion #181

Merged
merged 1 commit into from
Jul 5, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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