diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index 357f728d..bc91c332 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..f02b1662 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=''): 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/core/admin/mailu/ui/forms.py b/core/admin/mailu/ui/forms.py index 79c76450..c9bbb251 100644 --- a/core/admin/mailu/ui/forms.py +++ b/core/admin/mailu/ui/forms.py @@ -128,6 +128,12 @@ class UserPasswordForm(flask_wtf.FlaskForm): pwned = fields.HiddenField(label='', default=-1) submit = fields.SubmitField(_('Update password')) +class UserPasswordChangeForm(flask_wtf.FlaskForm): + current_pw = fields.PasswordField(_('Current password'), [validators.DataRequired()]) + pw = fields.PasswordField(_('Password'), [validators.DataRequired()]) + pw2 = fields.PasswordField(_('Password check'), [validators.DataRequired()]) + pwned = fields.HiddenField(label='', default=-1) + submit = fields.SubmitField(_('Update password')) class UserReplyForm(flask_wtf.FlaskForm): reply_enabled = fields.BooleanField(_('Enable automatic reply')) diff --git a/core/admin/mailu/ui/templates/sidebar.html b/core/admin/mailu/ui/templates/sidebar.html index 526b5908..3fd22f0e 100644 --- a/core/admin/mailu/ui/templates/sidebar.html +++ b/core/admin/mailu/ui/templates/sidebar.html @@ -20,7 +20,7 @@