DEV Community

Kanani Nirav
Kanani Nirav

Posted on • Edited on

10 2 1

Rack-attack gem setup to protect Rails and Rack apps from bad clients

Rack middleware for blocking & throttling abusive requests. Protect your Rails and Rack apps from bad clients. Rack::Attack lets you quickly decide when to allow, block, and throttle based on the properties of the request.

Using this gem you can save your web application from attacks, we can whitelist IPs, Block requests according to requirements, and many more…

Install Rack-attack gem:

# In your Gemfile
gem 'rack-attack'
Enter fullscreen mode Exit fullscreen mode

Plugging into the application

Then tell your ruby web application to use rack-attack as a middleware.

# config/application.rb
# rack attack middleware
config.middleware.use Rack::Attack
Enter fullscreen mode Exit fullscreen mode

Once you’ve done that, you’ll need to configure it. You can do this by creating the file, config/initializers/rack-attack.rband adding the rules to fit your needs.

You can disable it permanently (like for a specific environment) or temporarily (can be helpful for specific test cases) by writing:

# config/initializers/rack-attack.rb
Rack::Attack.enabled = ENV['ENABLE_RACK_ATTACK'] || Rails.env.production?
# Fallback to memory if we don't have Redis present or we're in test mode
if !ENV['REDIS_URL'] || Rails.env.test?
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
end
# Your Custom Rules here
view raw rack_attack.rb hosted with ❤ by GitHub

Usage

Safe listing

Safelists have the most precedence, so any request matching a safelist would be allowed despite matching any number of blocklists or throttles.

  • safelist_ip(ip_address_string)


Rack::Attack.safelist_ip(“5.6.7.8”)


Enter fullscreen mode Exit fullscreen mode
  • safelist_ip(ip_subnet_string)


Rack::Attack.safelist_ip(“5.6.7.0/24”)


Enter fullscreen mode Exit fullscreen mode
  • safelist(name, &block)

Name your custom safelist and make your ruby-block argument return a truthy value if you want the request to be allowed, and false otherwise.

# config/initializers/rack_attack.rb (for rails apps)
# Provided that trusted users use an HTTP request header named APIKey
Rack::Attack.safelist('mark any authenticated access safe') do |request|
# Requests are allowed if the return value is truthy
request.env['HTTP_APIKEY'] == 'secret-string'
end
# Always allow requests from localhost
# (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
view raw rack_attack.rb hosted with ❤ by GitHub

Blocking

  • blocklist_ip(ip_address_string)


 Rack::Attack.blocklist_ip(“1.2.3.4”)


Enter fullscreen mode Exit fullscreen mode
  • blocklist_ip(ip_subnet_string)


 Rack::Attack.blocklist_ip(“1.2.0.0/16”)


Enter fullscreen mode Exit fullscreen mode
  • blocklist(name, &block)

Name your custom blocklist and make your ruby-block argument return a truthy value if you want the request to be blocked, and false otherwise.

# config/initializers/rack_attack.rb (for rails apps)
Rack::Attack.blocklist('block all access to admin') do |request|
# Requests are blocked if the return value is truthy
request.path.start_with?('/admin')
end
Rack::Attack.blocklist('block bad UA logins') do |req|
req.path == '/login' && req.post? && req.user_agent == 'BadUA'
end
view raw rack_attack.rb hosted with ❤ by GitHub

Throttling

*throttle(name, options, &block) *( provide limit and period as options)

Throttle state is stored in a configurable cache (which defaults to Rails.cache if present).

Name your custom throttle, provide limit and period as options, and make your ruby-block argument return the discriminator. This discriminator is how you tell rack-attack whether you’re limiting per IP address, per user email, or any other.

  • For example, if we want to restrict requests other than defined routes and display a custom error page.
    Rack::Attack.blocklist('block all outer routes ') do |request|
    # Requests are blocked if the return value is truthy
    routes_array = Rails.application.routes.routes.flat_map { |r| r.path.spec.to_s }.uniq.map { |path| path&.gsub('(.:format)', '') }
    # allow assets url
    if request.path.start_with?('/assets')
    false
    else
    !routes_array.include?(request.path)
    end
    end
    # display custom error page
    Rack::Attack.blocklisted_response = lambda do |_env|
    html = ActionView::Base.empty.render(file: 'public/500.html')
    [403, {'Content-Type' => 'text/html'}, [html]]
    end
    view raw rack_attack.rb hosted with ❤ by GitHub
    Error page:

Error page

  • If we want to restrict requests/IP and if the request limit increases then send a reminder mail.

For Example, we want to allow only 300 requests per 30 seconds after that will restrict requests from this IP till the next 30 seconds interval starting.

# period in sec. it will allow 300 request per ip with in 30 second.
throttle('req/ip', limit: 300, period: 30) do |request|
request.ip unless request.path.start_with?('/assets')
end
ActiveSupport::Notifications.subscribe(/rack_attack/) do |name, start, finish, request_id, payload|
req = payload[:request]
if %i[throttle].include? req.env['rack.attack.match_type']
info = "Remote Ip: #{req.ip}"
rack_attack_throttle_data = req.env['rack.attack.throttle_data']['req/ip']
Rails.logger.info info
# your mail template
Notifier.rack_attack_error_mail(info, rack_attack_throttle_data&.stringify_keys).deliver
end
end
view raw rack_attack.rb hosted with ❤ by GitHub

Get error mail if the limit is extended.

Error mail
Performance

The overhead of running Rack::Attack is typically negligible (a few milliseconds per request), 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 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.

Sample rack-attack.rb file

# frozen_string_literal: true
# for filter requests
class Rack::Attack
Rack::Attack.blocklist('block all outer routes ') do |request|
# Requests are blocked if the return value is truthy
routes_array = Rails.application.routes.routes.flat_map { |r| r.path.spec.to_s }.uniq.map { |path| path&.gsub('(.:format)', '') }
# allow assets url
if request.path.start_with?('/assets')
false
else
!routes_array.include?(request.path)
end
end
throttle('req/ip', limit: 300, period: 30) do |request|
request.ip unless request.path.start_with?('/assets')
end
ActiveSupport::Notifications.subscribe(/rack_attack/) do |name, start, finish, request_id, payload|
req = payload[:request]
if %i[throttle].include? req.env['rack.attack.match_type']
info = "Remote Ip: #{req.ip}"
rack_attack_throttle_data = req.env['rack.attack.throttle_data']['req/ip']
Rails.logger.info info
Notifier.rack_attack_error_mail(info, rack_attack_throttle_data&.stringify_keys).deliver
end
end
Rack::Attack.blocklisted_response = lambda do |_env|
html = ActionView::Base.empty.render(file: 'public/500.html')
[403, { 'Content-Type' => 'text/html' }, [html]]
end
end
view raw rack_attack.rb hosted with ❤ by GitHub

For more information: https://github.com/rack/rack-attack

If You are using Medium Please support and follow me for interesting articles. Medium Profile

If this guide has been helpful to you and your team please share it with others!

Top comments (1)

Collapse
 
fabio_silva_10a7344bab8d6 profile image
Fabio Silva

routes_array do not work if route has parameters (:id), do not match with request.path