1278: Limiter implementation r=kaiyou a=micw

## What type of PR?

(Feature, enhancement, bug-fix, documentation)

## What does this PR do?

Adds a custom limter based on the "limits" lirary that counts up on failed auths only

### Related issue(s)
- closes #1195
- closes #634

## Prerequistes

- [X] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/guide.html#changelog) entry file.


Co-authored-by: Michael Wyraz <michael@wyraz.de>
Co-authored-by: micw <michael@wyraz.de>
master
bors[bot] 5 years ago committed by GitHub
commit 96f832835a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -32,6 +32,7 @@ DEFAULT_CONFIG = {
'POSTMASTER': 'postmaster', 'POSTMASTER': 'postmaster',
'TLS_FLAVOR': 'cert', 'TLS_FLAVOR': 'cert',
'AUTH_RATELIMIT': '10/minute;1000/hour', 'AUTH_RATELIMIT': '10/minute;1000/hour',
'AUTH_RATELIMIT_SUBNET': True,
'DISABLE_STATISTICS': False, 'DISABLE_STATISTICS': False,
# Mail settings # Mail settings
'DMARC_RUA': None, 'DMARC_RUA': None,

@ -1,4 +1,4 @@
from flask_limiter import RateLimitExceeded from mailu.limiter import RateLimitExceeded
from mailu import utils from mailu import utils
from flask import current_app as app from flask import current_app as app
@ -20,13 +20,4 @@ def rate_limit_handler(e):
return response return response
@utils.limiter.request_filter
def whitelist_webmail():
try:
return flask.request.headers["Client-Ip"] ==\
app.config["HOST_WEBMAIL"]
except:
return False
from mailu.internal.views import * from mailu.internal.views import *

@ -7,18 +7,19 @@ import flask_login
import base64 import base64
@internal.route("/auth/email") @internal.route("/auth/email")
@utils.limiter.limit(
lambda: app.config["AUTH_RATELIMIT"],
lambda: flask.request.headers["Client-Ip"]
)
def nginx_authentication(): def nginx_authentication():
""" Main authentication endpoint for Nginx email server """ Main authentication endpoint for Nginx email server
""" """
utils.limiter.check(flask.request.headers["Client-Ip"])
headers = nginx.handle_authentication(flask.request.headers) headers = nginx.handle_authentication(flask.request.headers)
response = flask.Response() response = flask.Response()
for key, value in headers.items(): for key, value in headers.items():
response.headers[key] = str(value) response.headers[key] = str(value)
if ("Auth-Status" not in headers) or (headers["Auth-Status"]!="OK"):
utils.limiter.hit(flask.request.headers["Client-Ip"])
return response return response

@ -0,0 +1,36 @@
import limits
import limits.storage
import limits.strategies
import ipaddress
class RateLimitExceeded(Exception):
pass
class Limiter:
def __init__(self):
self.storage = None
self.limiter = None
self.rate = None
self.subnet = None
self.rate_limit_subnet = True
def init_app(self, app):
self.storage = limits.storage.storage_from_string(app.config["RATELIMIT_STORAGE_URL"])
self.limiter = limits.strategies.MovingWindowRateLimiter(self.storage)
self.rate = limits.parse(app.config["AUTH_RATELIMIT"])
self.rate_limit_subnet = str(app.config["AUTH_RATELIMIT_SUBNET"])!='False'
self.subnet = ipaddress.ip_network(app.config["SUBNET"])
def check(self,clientip):
# disable limits for internal requests (e.g. from webmail)?
if self.rate_limit_subnet==False and ipaddress.ip_address(clientip) in self.subnet:
return
if not self.limiter.test(self.rate,"client-ip",clientip):
raise RateLimitExceeded()
def hit(self,clientip):
# disable limits for internal requests (e.g. from webmail)?
if self.rate_limit_subnet==False and ipaddress.ip_address(clientip) in self.subnet:
return
self.limiter.hit(self.rate,"client-ip",clientip)

@ -1,11 +1,10 @@
from mailu import models from mailu import models, limiter
import flask import flask
import flask_login import flask_login
import flask_script import flask_script
import flask_migrate import flask_migrate
import flask_babel import flask_babel
import flask_limiter
from werkzeug.contrib import fixers from werkzeug.contrib import fixers
@ -20,10 +19,8 @@ def handle_needs_login():
flask.url_for('ui.login', next=flask.request.endpoint) flask.url_for('ui.login', next=flask.request.endpoint)
) )
# Rate limiter
# Request rate limitation limiter = limiter.Limiter()
limiter = flask_limiter.Limiter(key_func=lambda: current_user.username)
# Application translation # Application translation
babel = flask_babel.Babel() babel = flask_babel.Babel()

@ -7,7 +7,7 @@ Flask-migrate
Flask-script Flask-script
Flask-wtf Flask-wtf
Flask-debugtoolbar Flask-debugtoolbar
Flask-limiter limits
redis redis
WTForms-Components WTForms-Components
socrate socrate

@ -38,8 +38,14 @@ recommended to setup a generic value and later configure a mail alias for that
address. address.
The ``AUTH_RATELIMIT`` holds a security setting for fighting attackers that The ``AUTH_RATELIMIT`` holds a security setting for fighting attackers that
try to guess user passwords. The value is the limit of requests that a single try to guess user passwords. The value is the limit of failed authentication attempts
IP address can perform against IMAP, POP and SMTP authentication endpoints. that a single IP address can perform against IMAP, POP and SMTP authentication endpoints.
If ``AUTH_RATELIMIT_SUBNET`` is ``True`` (which is the default), the ``AUTH_RATELIMIT``
rules does also apply to auth requests coming from ``SUBNET``, especially for the webmail.
If you disable this, ensure that the rate limit on the webmail is enforced in a different
way (e.g. roundcube plug-in), otherwise an attacker can simply bypass the limit using webmail.
The ``TLS_FLAVOR`` sets how Mailu handles TLS connections. Setting this value to The ``TLS_FLAVOR`` sets how Mailu handles TLS connections. Setting this value to
``notls`` will cause Mailu not to server any web content! More on :ref:`tls_flavor`. ``notls`` will cause Mailu not to server any web content! More on :ref:`tls_flavor`.

@ -0,0 +1,2 @@
Ratelimit counts up on failed auth only now
Loading…
Cancel
Save