You've already forked Mailu
mirror of
https://github.com/Mailu/Mailu.git
synced 2025-07-03 00:47:16 +02:00
Merge #2732
2732: Only account for distinct attempts in rate limits r=mergify[bot] a=nextgens ## What type of PR? enhancement ## What does this PR do? Only account for distinct attempts in rate limits. This is solving the problem related to users changing their passwords and having their client hammer the old credentials. Reduce the default to 50 distinct passwords per day ### Related issue(s) ## Prerequisites Before we can consider review and merge, please make sure the following list is done and checked. If an entry in not applicable, you can check it or remove it from the list. - [x] In case of feature or enhancement: documentation updated accordingly - [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file. Co-authored-by: Florent Daigniere <nextgens@freenetproject.org> Co-authored-by: Florent Daigniere <nextgens@users.noreply.github.com>
This commit is contained in:
@ -44,7 +44,7 @@ DEFAULT_CONFIG = {
|
|||||||
'AUTH_RATELIMIT_IP': '5/hour',
|
'AUTH_RATELIMIT_IP': '5/hour',
|
||||||
'AUTH_RATELIMIT_IP_V4_MASK': 24,
|
'AUTH_RATELIMIT_IP_V4_MASK': 24,
|
||||||
'AUTH_RATELIMIT_IP_V6_MASK': 48,
|
'AUTH_RATELIMIT_IP_V6_MASK': 48,
|
||||||
'AUTH_RATELIMIT_USER': '100/day',
|
'AUTH_RATELIMIT_USER': '50/day',
|
||||||
'AUTH_RATELIMIT_EXEMPTION': '',
|
'AUTH_RATELIMIT_EXEMPTION': '',
|
||||||
'AUTH_RATELIMIT_EXEMPTION_LENGTH': 86400,
|
'AUTH_RATELIMIT_EXEMPTION_LENGTH': 86400,
|
||||||
'DISABLE_STATISTICS': False,
|
'DISABLE_STATISTICS': False,
|
||||||
|
@ -85,6 +85,7 @@ def handle_authentication(headers):
|
|||||||
raw_user_email = urllib.parse.unquote(headers["Auth-User"])
|
raw_user_email = urllib.parse.unquote(headers["Auth-User"])
|
||||||
raw_password = urllib.parse.unquote(headers["Auth-Pass"])
|
raw_password = urllib.parse.unquote(headers["Auth-Pass"])
|
||||||
user_email = 'invalid'
|
user_email = 'invalid'
|
||||||
|
password = 'invalid'
|
||||||
try:
|
try:
|
||||||
user_email = raw_user_email.encode("iso8859-1").decode("utf8")
|
user_email = raw_user_email.encode("iso8859-1").decode("utf8")
|
||||||
password = raw_password.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-Server": server,
|
||||||
"Auth-User": user_email,
|
"Auth-User": user_email,
|
||||||
"Auth-User-Exists": is_valid_user,
|
"Auth-User-Exists": is_valid_user,
|
||||||
|
"Auth-Password": password,
|
||||||
"Auth-Port": port
|
"Auth-Port": port
|
||||||
}
|
}
|
||||||
status, code = get_status(protocol, "authentication")
|
status, code = get_status(protocol, "authentication")
|
||||||
@ -115,6 +117,7 @@ def handle_authentication(headers):
|
|||||||
"Auth-Error-Code": code,
|
"Auth-Error-Code": code,
|
||||||
"Auth-User": user_email,
|
"Auth-User": user_email,
|
||||||
"Auth-User-Exists": is_valid_user,
|
"Auth-User-Exists": is_valid_user,
|
||||||
|
"Auth-Password": password,
|
||||||
"Auth-Wait": 0
|
"Auth-Wait": 0
|
||||||
}
|
}
|
||||||
# Unexpected
|
# Unexpected
|
||||||
|
@ -48,7 +48,7 @@ def nginx_authentication():
|
|||||||
if headers.get("Auth-Status") == "OK":
|
if headers.get("Auth-Status") == "OK":
|
||||||
utils.limiter.exempt_ip_from_ratelimits(client_ip)
|
utils.limiter.exempt_ip_from_ratelimits(client_ip)
|
||||||
elif is_valid_user:
|
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:
|
elif not is_from_webmail:
|
||||||
utils.limiter.rate_limit_ip(client_ip, username)
|
utils.limiter.rate_limit_ip(client_ip, username)
|
||||||
return response
|
return response
|
||||||
|
@ -68,9 +68,13 @@ class LimitWraperFactory(object):
|
|||||||
app.logger.warn(f'Authentication attempt from {ip} for {username} has been rate-limited.')
|
app.logger.warn(f'Authentication attempt from {ip} for {username} has been rate-limited.')
|
||||||
return is_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')
|
limiter = self.get_limiter(app.config["AUTH_RATELIMIT_USER"], 'auth-user')
|
||||||
if self.is_subject_to_rate_limits(ip):
|
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)
|
limiter.hit(device_cookie if device_cookie_name == username else username)
|
||||||
|
|
||||||
""" Device cookies as described on:
|
""" Device cookies as described on:
|
||||||
|
@ -59,7 +59,7 @@ def login():
|
|||||||
flask.flash(msg, "error")
|
flask.flash(msg, "error")
|
||||||
return response
|
return response
|
||||||
else:
|
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.current_app.logger.warn(f'Login failed for {username} from {client_ip}.')
|
||||||
flask.flash('Wrong e-mail or password', 'error')
|
flask.flash('Wrong e-mail or password', 'error')
|
||||||
return flask.render_template('login.html', form=form, fields=fields)
|
return flask.render_template('login.html', form=form, fields=fields)
|
||||||
|
@ -47,10 +47,11 @@ accounts for a specific IP subnet as defined in
|
|||||||
``AUTH_RATELIMIT_IP_V4_MASK`` (default: /24) and
|
``AUTH_RATELIMIT_IP_V4_MASK`` (default: /24) and
|
||||||
``AUTH_RATELIMIT_IP_V6_MASK`` (default: /48).
|
``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
|
attackers that attempt to guess a user's password (typically using a password
|
||||||
bruteforce attack). The value defines the limit of authentication attempts allowed
|
bruteforce attack). The value defines the limit of distinct authentication attempts
|
||||||
for any given account within a specific timeframe.
|
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
|
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.
|
after a successful login for which a specific IP address is exempted from rate limits.
|
||||||
|
@ -47,7 +47,7 @@ Or in plain english: if receivers start to classify your mail as spam, this post
|
|||||||
<label>Authentication rate limit per user</label>
|
<label>Authentication rate limit per user</label>
|
||||||
<!-- Validates number input only -->
|
<!-- Validates number input only -->
|
||||||
<p><input class="form-control" style="width: 9%; display: inline;" type="number" name="auth_ratelimit_user"
|
<p><input class="form-control" style="width: 9%; display: inline;" type="number" name="auth_ratelimit_user"
|
||||||
value="100" required > / day
|
value="50" required > / day
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
1
towncrier/newsfragments/2726.misc
Normal file
1
towncrier/newsfragments/2726.misc
Normal file
@ -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.
|
Reference in New Issue
Block a user