Skip to content

Commit 0eb08f6

Browse files
committed
Add initial rack-attack configuration to help mitigate automated attacks
1 parent 5166344 commit 0eb08f6

File tree

4 files changed

+120
-0
lines changed

4 files changed

+120
-0
lines changed

Gemfile.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ PATH
4949
premailer-rails
5050
pundit (>= 2.1, < 2.6)
5151
pundit-resources
52+
rack-attack
5253
rack-cors (>= 1.1.1, < 2.1.0)
5354
rack-mini-profiler
5455
rails (>= 5.2.2, < 7.2.0)
@@ -507,6 +508,8 @@ GEM
507508
activesupport (>= 3.0.0)
508509
racc (1.8.1)
509510
rack (3.1.13)
511+
rack-attack (6.7.0)
512+
rack (>= 1.0, < 4)
510513
rack-cors (2.0.2)
511514
rack (>= 2.0.0)
512515
rack-mini-profiler (3.3.1)

better_together.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Gem::Specification.new do |spec|
5757
spec.add_dependency 'premailer-rails'
5858
spec.add_dependency 'pundit', '>= 2.1', '< 2.6'
5959
spec.add_dependency 'pundit-resources'
60+
spec.add_dependency 'rack-attack'
6061
spec.add_dependency 'rack-cors', '>= 1.1.1', '< 2.1.0'
6162
spec.add_dependency 'rack-mini-profiler'
6263
spec.add_dependency 'rails', '>= 5.2.2', '< 7.2.0'

config/initializers/rack_attack.rb

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# frozen_string_literal: true
2+
3+
module Rack
4+
# Sets default Rack::Attack configuration
5+
class Attack
6+
### Configure Cache ###
7+
8+
# If you don't want to use Rails.cache (Rack::Attack's default), then
9+
# configure it here.
10+
#
11+
# Note: The store is only used for throttling (not blocklisting and
12+
# safelisting). It must implement .increment and .write like
13+
# ActiveSupport::Cache::Store
14+
15+
rack_attack_redis = ENV.fetch('RACK_ATTACK_REDIS_URL', nil)
16+
17+
# Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
18+
if rack_attack_redis
19+
Rack::Attack.cache.store = ActiveSupport::Cache::RedisCacheStore.new(
20+
url: rack_attack_redis
21+
)
22+
end
23+
24+
### Throttle Spammy Clients ###
25+
26+
# If any single client IP is making tons of requests, then they're
27+
# probably malicious or a poorly-configured scraper. Either way, they
28+
# don't deserve to hog all of the app server's CPU. Cut them off!
29+
#
30+
# Note: If you're serving assets through rack, those requests may be
31+
# counted by rack-attack and this throttle may be activated too
32+
# quickly. If so, enable the condition to exclude them from tracking.
33+
34+
# Throttle all requests by IP (60rpm)
35+
#
36+
# Key: "rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}"
37+
throttle('req/ip', limit: 300, period: 5.minutes, &:ip)
38+
39+
### Prevent Brute-Force Login Attacks ###
40+
41+
# The most common brute-force login attack is a brute-force password
42+
# attack where an attacker simply tries a large number of emails and
43+
# passwords to see if any credentials match.
44+
#
45+
# Another common method of attack is to use a swarm of computers with
46+
# different IPs to try brute-forcing a password for a specific account.
47+
48+
# Throttle POST requests to /users/sign-in by IP address
49+
#
50+
# Key: "rack::attack:#{Time.now.to_i/:period}:logins/ip:#{req.ip}"
51+
throttle('logins/ip', limit: 5, period: 20.seconds) do |req|
52+
req.ip if req.path.include?('/users/sign-in') && req.post?
53+
end
54+
55+
# Throttle POST requests to /users/sign-in by email param
56+
#
57+
# Key: "rack::attack:#{Time.now.to_i/:period}:logins/email:#{normalized_email}"
58+
#
59+
# Note: This creates a problem where a malicious user could intentionally
60+
# throttle logins for another user and force their login requests to be
61+
# denied, but that's not very common and shouldn't happen to you. (Knock
62+
# on wood!)
63+
throttle('logins/email', limit: 5, period: 20.seconds) do |req|
64+
if req.path.include?('/users/sign-in') && req.post?
65+
# Normalize the email, using the same logic as your authentication process, to
66+
# protect against rate limit bypasses. Return the normalized email if present, nil otherwise.
67+
req.params['email'].to_s.downcase.gsub(/\s+/, '').presence
68+
end
69+
end
70+
71+
### Fail2Ban for PHP Files ###
72+
73+
# Block requests for .php files, which are often targeted in WordPress attacks.
74+
blocklist('fail2ban/php-files') do |req|
75+
req.path.end_with?('.php')
76+
end
77+
78+
# Block suspicious requests for '/etc/password' or wordpress specific paths.
79+
# After 3 blocked requests in 10 minutes, block all requests from that IP for 5 minutes.
80+
blocklist('fail2ban pentesters') do |req|
81+
# `filter` returns truthy value if request fails, or if it's from a previously banned IP
82+
# so the request is blocked
83+
Rack::Attack::Fail2Ban.filter("pentesters-#{req.ip}", maxretry: 3, findtime: 10.minutes, bantime: 5.minutes) do
84+
# The count for the IP is incremented if the return value is truthy
85+
CGI.unescape(req.query_string) =~ %r{/etc/passwd} ||
86+
req.path.include?('/etc/passwd') ||
87+
req.path.include?('wp-admin') ||
88+
req.path.include?('wp-content') ||
89+
req.path.include?('wp-files') ||
90+
req.path.include?('wp-login') ||
91+
req.path.include?('.php')
92+
end
93+
end
94+
95+
### Custom Throttle Response ###
96+
97+
# By default, Rack::Attack returns an HTTP 429 for throttled responses,
98+
# which is just fine.
99+
#
100+
# If you want to return 503 so that the attacker might be fooled into
101+
# believing that they've successfully broken your app (or you just want to
102+
# customize the response), then uncomment these lines.
103+
self.throttled_responder = lambda do |_env|
104+
[503, # status
105+
{}, # headers
106+
['']] # body
107+
end
108+
109+
self.blocklisted_responder = lambda do |_request|
110+
# Using 503 because it may make attacker think that they have successfully
111+
# DOSed the site. Rack::Attack returns 403 for blocklists by default
112+
[503, {}, ['Blocked']]
113+
end
114+
end
115+
end

lib/better_together/engine.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
require 'kaminari'
2626
require 'noticed'
2727
require 'premailer/rails'
28+
require 'rack/attack'
2829
require 'reform/rails'
2930
require 'ruby/openai'
3031
require 'simple_calendar'

0 commit comments

Comments
 (0)