How to protect your rails App from bad clients.

Juan David Gaviria
kommit
Published in
5 min readJun 23, 2020

Some months ago, I was working on an implementation to protect my rails app from bad clients or Scrapers, they make a lot of requests in a short period of time, so I started to research some tools(Heroku addon’s) but these tools are expensive, then I found the rack-attack gem(middleware for blocking abusive requests) and decided to develop my implementation with this gem.

In this tutorial, I want to show you step by step, how to configure and work with rack-attack gem, We will use the following versions:

  • Rails 6.0.0
  • Ruby 2.6.3

We’re going to create our project:

rails _6.0.0_ new my_rack_attack -T -d postgresql 
cd my_rack_attac
rails db:setup

Then we can run our server:

rails s

Obtain in http://localhost:3000/ :

We’ll use the Rails scaffold to create a complete CRUD of posts:

rails g scaffold Post title  
rails db:migrate

We add the following line to our routes ⇒ config/routes.rb:

root to: "posts#index"

If we refresh our browser:

We can add some example posts, We will use the seed generator of rails, We have to edit db/seeds.rb:

10.times do |i|   
Post.create(title: "Post #{i}")
end

Now we run:

rails db:seed

We will suppose that we want to protect this page.

Later We need to add rack-attack gem to our Gemfile and figaro gem that allow us to work with environment variables:

gem 'rack-attack', '~> 6.2', '>= 6.2.2' 
gem 'figaro'

Then we have to run:

bundle install

We tell our app to use rack-attack as a middleware in config/application.rb:

config.middleware.use Rack::Attack

We’ll create our file to manage all environment variables in config/application.yml and We’ll add the first variable:

RACK_ATTACK_BLOCK_IPS: 127.0.0.1

We test our environment variables in a rails console:

We will manage all protection rules in an initializer file, so we have to create new file config/initializers/rack_attack.rb.

Case 1

In this case, we suppose that we know the IP address of the bad user, so we create a blacklist, add the following code in rack_attack.rb:

class Rack::Attack
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new

spammers = ENV['RACK_ATTACK_BLOCK_IPS'].try(:split, /,\s*/)
if spammers.present?
spammer_regexp = Regexp.union(spammers)
Rack::Attack.blocklist('block spam ip') do |req|
req.ip =~ spammer_regexp
end
end
end

In the previous code, we get the environment variable RACK_ATTACK_BLOCK_IPS and generate an array of IP’s(blacklist) and use the method blocklist of the Rack::Attack class to block all IP’s in the list and the bad user obtains a 403 response(forbidden), we have modified an initializer so we have to restart the server, If we visit http://127.0.0.1:3000/:

Case 2

How to block spammers or bad users when trying to make a lot of requests in a short time and we don´t know the IP of the attack origin, we add three more EV in aplication.yml:

RACK_ATTACK_RETRY: 2 
RACK_ATTACK_FIND_TIME: 10
RACK_ATTACK_BAND_TIME: 60

The above variables mean

RACK_ATTACK_RETRY ⇒ The number of requests that we will allow in a time frame..

RACK_ATTACK_FIND_TIME ⇒ Period of time(seconds) that rack-attack analyze the number of requests

RACK_ATTACK_BAND_TIME ⇒ Period of time(seconds) that we will block our app for the attacker.

In conclusion, for this case, if a user or spammer makes more than two requests in 10 seconds, this user will be blocked for 60 seconds(this configuration will depend on each project), add the following code in initializers/rack-attack.rb:

attack_ban_retries  = ENV['RACK_ATTACK_RETRY'].try(:to_i) || 0
attack_ban_findtime = ENV['RACK_ATTACK_FIND_TIME'].try(:to_i) || 10
attack_ban_bandtime = ENV['RACK_ATTACK_BAND_TIME'].try(:to_i) || 60
if attack_ban_retries > 0
Rack::Attack.blocklist('allow2ban login scrapers') do |req|
Rack::Attack::Allow2Ban.filter(req.ip, maxretry: attack_ban_retries, findtime: attack_ban_findtime, bantime: attack_ban_bandtime) do
req.path == '/'
end
end
end
ActiveSupport::Notifications.subscribe(/rack_attack/) do |name, start, finish, request_id, payload| req = payload[:request]
remote_ip = req.env["HTTP_X_FORWARDED_FOR"].presence || req.env["REMOTE_ADDR"].presence
url = req.env["REQUEST_URI"].presence
baned_time = ENV['RACK_ATTACK_BAND_TIME'].try(:to_i)
if %i[throttle blocklist].include?(req.env['rack.attack.match_type'])
Rails.logger.info "[Rack::Attack][PostApp]" <<
" remote_ip: \"#{remote_ip}\"," <<
" banned time: #{baned_time} seconds"
end
end

In the previous code, we use the filter Allow2Ban(allowing the behavior previous described) and the module ActiveSupport::Notifications to consume notifications(in this case only for an informative log with the IP and the band time, but you could add more logic, for example send an alert email, or save all attack information ), if we restart de server and we make more than 2 requests for 10 seconds we will be banned the app for 60 seconds:

In this image, we can check the informative log, later to make more than two requests in 10 seconds, the app returns 403 response, we have to wait for 1 minute to use again the app.

We could finish with a custom response:

Rack::Attack.blocklisted_response = lambda do |env|     
[ 503, {}, ["Server Error\n"]]
end

The final rack-attack.rb file is:

Rack Attack has a lot of functionalities, It’s up to you to use the best option.

References

Credits

Cover image Designed by Freepik

--

--