Skip to content

Instantly share code, notes, and snippets.

@johnmcdowall
Created January 16, 2024 21:58
Show Gist options
  • Save johnmcdowall/4da61d72cb0e976ceb0643b222d07839 to your computer and use it in GitHub Desktop.
Save johnmcdowall/4da61d72cb0e976ceb0643b222d07839 to your computer and use it in GitHub Desktop.
require "json"
Rack::Attack.safelist("allow RSpec") { |_req| Rails.env.test? }
Rack::Attack.safelist("allow development") { |req| Rails.env.development? }
# Allow the other containers on the server unlimited access, tweak IP range for your situation
Rack::Attack.safelist_ip("10.10.0.0/24")
# Limit data modification requests
Rack::Attack.throttle("unsafe/req/ip", limit: 30, period: 60.seconds) do |req|
req.remote_ip unless req.get?
end
Rack::Attack.track("admin") do |req|
req.path.start_with?("/admin")
end
Rack::Attack.throttle("authentication/ip", limit: 30, period: 1.hour) do |req|
if req.post? &&
%w[/users/sign_in /users/sign_up /users/password].include?(
req.path
)
req.remote_ip
end
end
# 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.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.remote_ip}", maxretry: 3, findtime: 30.minutes, bantime: 60.minutes) do
# The count for the IP is incremented if the return value is truthy
CGI.unescape(req.query_string) =~ %r{/etc/passwd} ||
req.path.include?("/etc/passwd") ||
req.path.include?("wp-admin") ||
req.path.include?("wp-login") ||
req.path.include?("wp-includes") ||
req.path.include?("xmlrpc") ||
req.path.include?("php") ||
req.path.include?(".git") ||
req.path.include?(".env")
end
end
Rack::Attack.blocklisted_responder = lambda do |_request|
# Using 503 because it may make attacker think that they have successfully
# DOSed the site. Rack::Attack returns 403 for blocklists by default
[503, {}, ["Server Error"]]
end
Rack::Attack.throttled_responder = lambda do |_request|
# NB: you have access to the name and other data about the matched throttle
# request.env['rack.attack.matched'],
# request.env['rack.attack.match_type'],
# request.env['rack.attack.match_data'],
# request.env['rack.attack.match_discriminator']
[503, {}, ["Server Error\n"]]
end
ActiveSupport::Notifications.subscribe("throttle.rack_attack") do |_name, _start, _finish, _request_id, payload|
req = payload[:request]
rack_attack_throttle_data = req.env["rack.attack.throttle_data"]
Rails.logger.info "[RACK ATTACK] #{req.env["rack.attack.match_type"]} #{req.remote_ip} on #{req.request_method} #{req.fullpath} #{req.env["rack.attack.match_discriminator"]} reason: #{req.env["rack.attack.matched"]}"
Rails.logger.info rack_attack_throttle_data&.stringify_keys.inspect
end
ActiveSupport::Notifications.subscribe("blocklist.rack_attack") do |_name, _start, _finish, _request_id, payload|
req = payload[:request]
Rails.logger.info "[RACK ATTACK] #{req.env["rack.attack.match_type"]} #{req.remote_ip} on #{req.request_method} #{req.fullpath} #{req.env["rack.attack.match_discriminator"]} reason: #{req.env["rack.attack.matched"]}"
end
ActiveSupport::Notifications.subscribe("track.rack_attack") do |_name, _start, _finish, _request_id, payload|
req = payload[:request]
if Rails.env.production? && req.env["rack.attack.matched"] == "admin"
Rails.logger.info "[RACK ATTACK] Request to admin: #{req.path} from #{req.remote_ip}"
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment