From e2a25c79fc0fecce6be4e095efb35610c3beeb8a Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 4 Feb 2023 16:32:51 +0100 Subject: [PATCH] only account attempts for distinct usernames in ratelimits --- core/admin/mailu/internal/views/auth.py | 6 +++--- core/admin/mailu/limiter.py | 7 +++++-- core/admin/mailu/sso/views/base.py | 2 +- towncrier/newsfragments/2644.misc | 1 + 4 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 towncrier/newsfragments/2644.misc diff --git a/core/admin/mailu/internal/views/auth.py b/core/admin/mailu/internal/views/auth.py index 5f5f8821..435defb5 100644 --- a/core/admin/mailu/internal/views/auth.py +++ b/core/admin/mailu/internal/views/auth.py @@ -33,8 +33,8 @@ def nginx_authentication(): for key, value in headers.items(): response.headers[key] = str(value) is_valid_user = False + username = response.headers['Auth-User'] if response.headers.get("Auth-User-Exists") == "True": - username = response.headers["Auth-User"] if utils.limiter.should_rate_limit_user(username, client_ip): # FIXME could be done before handle_authentication() status, code = nginx.get_status(flask.request.headers['Auth-Protocol'], 'ratelimit') @@ -50,7 +50,7 @@ def nginx_authentication(): elif is_valid_user: utils.limiter.rate_limit_user(username, client_ip) elif not is_from_webmail: - utils.limiter.rate_limit_ip(client_ip) + utils.limiter.rate_limit_ip(client_ip, username) return response @internal.route("/auth/admin") @@ -109,7 +109,7 @@ def basic_authentication(): utils.limiter.exempt_ip_from_ratelimits(client_ip) return response # We failed check_credentials - utils.limiter.rate_limit_user(user_email, client_ip) if user else utils.limiter.rate_limit_ip(client_ip) + utils.limiter.rate_limit_user(user_email, client_ip) if user else utils.limiter.rate_limit_ip(client_ip, user_email) response = flask.Response(status=401) response.headers["WWW-Authenticate"] = 'Basic realm="Login Required"' return response diff --git a/core/admin/mailu/limiter.py b/core/admin/mailu/limiter.py index be4199d2..31ed317a 100644 --- a/core/admin/mailu/limiter.py +++ b/core/admin/mailu/limiter.py @@ -52,10 +52,13 @@ class LimitWraperFactory(object): app.logger.warn(f'Authentication attempt from {ip} has been rate-limited.') return is_rate_limited - def rate_limit_ip(self, ip): - limiter = self.get_limiter(app.config["AUTH_RATELIMIT_IP"], 'auth-ip') + def rate_limit_ip(self, ip, username=None): + limiter = self.get_limiter(app.config['AUTH_RATELIMIT_IP'], 'auth-ip') client_network = utils.extract_network_from_ip(ip) if self.is_subject_to_rate_limits(ip): + if username and (self.storage.get(f'dedup-{ip}-{username}') > 0): + return + self.storage.incr(f'dedup-{ip}-{username}', limits.parse(app.config['AUTH_RATELIMIT_IP']).GRANULARITY.seconds ,True) limiter.hit(client_network) def should_rate_limit_user(self, username, ip, device_cookie=None, device_cookie_name=None): diff --git a/core/admin/mailu/sso/views/base.py b/core/admin/mailu/sso/views/base.py index aa0e9ea9..f8fd5e10 100644 --- a/core/admin/mailu/sso/views/base.py +++ b/core/admin/mailu/sso/views/base.py @@ -47,7 +47,7 @@ def login(): flask.flash(msg, "error") return response else: - utils.limiter.rate_limit_user(username, client_ip, device_cookie, device_cookie_username) if models.User.get(username) else utils.limiter.rate_limit_ip(client_ip) + utils.limiter.rate_limit_user(username, client_ip, device_cookie, device_cookie_username) if models.User.get(username) else utils.limiter.rate_limit_ip(client_ip, username) flask.current_app.logger.warn(f'Login failed for {username} from {client_ip}.') flask.flash('Wrong e-mail or password', 'error') return flask.render_template('login.html', form=form, fields=fields) diff --git a/towncrier/newsfragments/2644.misc b/towncrier/newsfragments/2644.misc new file mode 100644 index 00000000..8a20b39b --- /dev/null +++ b/towncrier/newsfragments/2644.misc @@ -0,0 +1 @@ +Implement de-dupplication on rate limits. Now only attempts for distinct usernames will count as a hit.