mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Add rack-attack throttler for all logins
We have a built-in bruteforce protection for built-in users. When users are being created from LDAP on-the-fly, these limits cannot apply, as we do not have a user object yet. Instead, we can provide a more generous throttler to block attempts
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OpenProject
|
||||
module RateLimiting
|
||||
module_function
|
||||
@@ -9,13 +11,15 @@ module OpenProject
|
||||
def default_rules
|
||||
@default_rules ||= [
|
||||
LostPassword,
|
||||
APIV3
|
||||
APIV3,
|
||||
Login
|
||||
]
|
||||
end
|
||||
|
||||
def set_defaults!
|
||||
Rack::Attack.clear_configuration
|
||||
Rack::Attack.throttled_responder = ->(request) { OpenProject::RateLimiting.throttled_response(request) }
|
||||
Rack::Attack.blocklisted_responder = ->(request) { OpenProject::RateLimiting.blocklisted_response(request) }
|
||||
|
||||
@active_rules = []
|
||||
default_rules.each do |rule|
|
||||
@@ -31,17 +35,20 @@ module OpenProject
|
||||
active_rules << rule.new.apply! if rule.enabled?
|
||||
end
|
||||
|
||||
##
|
||||
# Try to find a matching rule to respond with
|
||||
# or use the default responder
|
||||
def throttled_response(request)
|
||||
rule = active_rules.find { |r| r.rule_name == request.env["rack.attack.matched"] }
|
||||
matched = request.env["rack.attack.matched"]
|
||||
rule = find_rule(matched)
|
||||
rule ? rule.response(request) : Base.new.response(request)
|
||||
end
|
||||
|
||||
if rule
|
||||
rule.response(request)
|
||||
else
|
||||
OpenProject::RateLimiting::Base.response(request)
|
||||
end
|
||||
def blocklisted_response(request)
|
||||
matched = request.env["rack.attack.matched"]
|
||||
rule = find_rule(matched)
|
||||
rule ? rule.blocked_response : [403, {}, ["Forbidden\n"]]
|
||||
end
|
||||
|
||||
def find_rule(matched)
|
||||
active_rules.find { |r| matched == r.rule_name || matched.start_with?("#{r.rule_name}/") }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OpenProject
|
||||
module RateLimiting
|
||||
class Base
|
||||
@@ -54,6 +56,14 @@ module OpenProject
|
||||
"Your request has been throttled. Try again at #{retry_after.seconds.from_now}.\n"
|
||||
end
|
||||
|
||||
def blocked_response
|
||||
[429, { "Content-Type" => "text/plain" }, [blocked_response_body]]
|
||||
end
|
||||
|
||||
def blocked_response_body
|
||||
"Your request has been blocked.\n"
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Provide a limit callback proc for the request, or use the default limit
|
||||
@@ -91,7 +101,7 @@ module OpenProject
|
||||
false
|
||||
end
|
||||
|
||||
def discriminator(request)
|
||||
def discriminator(_request)
|
||||
raise SubclassResponsibilityError
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OpenProject
|
||||
module RateLimiting
|
||||
# Per-account HTTP-layer brute-force protection for POST /login.
|
||||
#
|
||||
# Uses Rack::Attack::Allow2Ban: the first BURST_LIMIT attempts within
|
||||
# BURST_PERIOD are allowed through; once the limit is exceeded a ban flag
|
||||
# is written that blocks all subsequent attempts for BAN_PERIOD.
|
||||
#
|
||||
# Enabled by default. Disable or tune via configuration.yml:
|
||||
#
|
||||
# rate_limiting:
|
||||
# login:
|
||||
# enabled: false
|
||||
class Login < Base
|
||||
BURST_LIMIT = 20
|
||||
BURST_PERIOD = 1.minute.to_i
|
||||
BAN_PERIOD = 30.minutes.to_i
|
||||
|
||||
class << self
|
||||
def enabled_by_default?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def apply!
|
||||
Rack::Attack.blocklist(rule_name) do |req|
|
||||
next false unless req.post? && req.path == "/login"
|
||||
|
||||
username = req.env.dig("rack.request.form_hash", "username").to_s.downcase.presence
|
||||
next false unless username
|
||||
|
||||
Rack::Attack::Allow2Ban.filter(
|
||||
"login:#{username}",
|
||||
maxretry: burst_limit,
|
||||
findtime: burst_period,
|
||||
bantime: ban_period
|
||||
) { true }
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def blocked_response_body
|
||||
"Too many login attempts. Please try again later.\n"
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def burst_limit
|
||||
settings[:burst_limit].presence&.to_i || BURST_LIMIT
|
||||
end
|
||||
|
||||
def burst_period
|
||||
settings[:burst_period].presence&.to_i || BURST_PERIOD
|
||||
end
|
||||
|
||||
def ban_period
|
||||
settings[:ban_period].presence&.to_i || BAN_PERIOD
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user