diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index 3adf9a6c..2603bf38 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -44,7 +44,7 @@ DEFAULT_CONFIG = { 'AUTH_RATELIMIT_IP': '5/hour', 'AUTH_RATELIMIT_IP_V4_MASK': 24, 'AUTH_RATELIMIT_IP_V6_MASK': 48, - 'AUTH_RATELIMIT_USER': '100/day', + 'AUTH_RATELIMIT_USER': '50/day', 'AUTH_RATELIMIT_EXEMPTION': '', 'AUTH_RATELIMIT_EXEMPTION_LENGTH': 86400, 'DISABLE_STATISTICS': False, diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py index 577e5a44..d0d11052 100644 --- a/core/admin/mailu/internal/nginx.py +++ b/core/admin/mailu/internal/nginx.py @@ -85,6 +85,7 @@ def handle_authentication(headers): raw_user_email = urllib.parse.unquote(headers["Auth-User"]) raw_password = urllib.parse.unquote(headers["Auth-Pass"]) user_email = 'invalid' + password = 'invalid' try: user_email = raw_user_email.encode("iso8859-1").decode("utf8") password = raw_password.encode("iso8859-1").decode("utf8") @@ -107,6 +108,7 @@ def handle_authentication(headers): "Auth-Server": server, "Auth-User": user_email, "Auth-User-Exists": is_valid_user, + "Auth-Password": password, "Auth-Port": port } status, code = get_status(protocol, "authentication") @@ -115,6 +117,7 @@ def handle_authentication(headers): "Auth-Error-Code": code, "Auth-User": user_email, "Auth-User-Exists": is_valid_user, + "Auth-Password": password, "Auth-Wait": 0 } # Unexpected diff --git a/core/admin/mailu/internal/views/auth.py b/core/admin/mailu/internal/views/auth.py index 27e8861c..01d1562f 100644 --- a/core/admin/mailu/internal/views/auth.py +++ b/core/admin/mailu/internal/views/auth.py @@ -48,7 +48,7 @@ def nginx_authentication(): if headers.get("Auth-Status") == "OK": utils.limiter.exempt_ip_from_ratelimits(client_ip) elif is_valid_user: - utils.limiter.rate_limit_user(username, client_ip) + utils.limiter.rate_limit_user(username, client_ip, password=response.headers.get('Auth-Password', None)) elif not is_from_webmail: utils.limiter.rate_limit_ip(client_ip, username) return response diff --git a/core/admin/mailu/limiter.py b/core/admin/mailu/limiter.py index d8b36111..f8e90101 100644 --- a/core/admin/mailu/limiter.py +++ b/core/admin/mailu/limiter.py @@ -68,9 +68,13 @@ class LimitWraperFactory(object): app.logger.warn(f'Authentication attempt from {ip} for {username} has been rate-limited.') return is_rate_limited - def rate_limit_user(self, username, ip, device_cookie=None, device_cookie_name=None): + def rate_limit_user(self, username, ip, device_cookie=None, device_cookie_name=None, password=None): limiter = self.get_limiter(app.config["AUTH_RATELIMIT_USER"], 'auth-user') if self.is_subject_to_rate_limits(ip): + truncated_password = hmac.new(bytearray(username, 'utf-8'), bytearray(password, 'utf-8'), 'sha256').hexdigest()[-6:] + if password and self.storage.get(f'dedup2-{username}-{truncated_password}') > 0: + return + self.storage.incr(f'dedup2-{username}-{truncated_password}', limits.parse(app.config['AUTH_RATELIMIT_USER']).GRANULARITY.seconds, True) limiter.hit(device_cookie if device_cookie_name == username else username) """ Device cookies as described on: diff --git a/core/admin/mailu/sso/views/base.py b/core/admin/mailu/sso/views/base.py index 43237c75..42175f05 100644 --- a/core/admin/mailu/sso/views/base.py +++ b/core/admin/mailu/sso/views/base.py @@ -59,7 +59,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, username) + utils.limiter.rate_limit_user(username, client_ip, device_cookie, device_cookie_username, form.pw.data) 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/docs/configuration.rst b/docs/configuration.rst index e4bac5ff..56801ec8 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -47,10 +47,11 @@ accounts for a specific IP subnet as defined in ``AUTH_RATELIMIT_IP_V4_MASK`` (default: /24) and ``AUTH_RATELIMIT_IP_V6_MASK`` (default: /48). -The ``AUTH_RATELIMIT_USER`` (default: 100/day) holds a security setting for fighting +The ``AUTH_RATELIMIT_USER`` (default: 50/day) holds a security setting for fighting attackers that attempt to guess a user's password (typically using a password -bruteforce attack). The value defines the limit of authentication attempts allowed -for any given account within a specific timeframe. +bruteforce attack). The value defines the limit of distinct authentication attempts +allowed for any given account within a specific timeframe. Multiple attempts for the +same account with the same password only counts for one. The ``AUTH_RATELIMIT_EXEMPTION_LENGTH`` (default: 86400) is the number of seconds after a successful login for which a specific IP address is exempted from rate limits. diff --git a/setup/templates/steps/config.html b/setup/templates/steps/config.html index b3b5fe87..025a2a2c 100644 --- a/setup/templates/steps/config.html +++ b/setup/templates/steps/config.html @@ -47,7 +47,7 @@ Or in plain english: if receivers start to classify your mail as spam, this post
/ day + value="50" required > / day
diff --git a/towncrier/newsfragments/2726.misc b/towncrier/newsfragments/2726.misc new file mode 100644 index 00000000..f7eb41e3 --- /dev/null +++ b/towncrier/newsfragments/2726.misc @@ -0,0 +1 @@ +Change the behaviour of AUTH_RATELIMIT_USER and only account for distinct attempts. Same username and same password is now a only accounted once per period.