From 5f94be871cbad0196689e2330c3c6b6d39a625f9 Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Tue, 30 May 2023 14:09:16 +0000 Subject: [PATCH 1/6] Authentication failed for email clients when the password contained a non latin-1 character. --- core/admin/mailu/internal/nginx.py | 2 +- towncrier/newsfragments/2837.bugfix | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 towncrier/newsfragments/2837.bugfix diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py index b76e9a89..dfc24a02 100644 --- a/core/admin/mailu/internal/nginx.py +++ b/core/admin/mailu/internal/nginx.py @@ -111,7 +111,7 @@ def handle_authentication(headers): "Auth-Server": server, "Auth-User": user_email, "Auth-User-Exists": is_valid_user, - "Auth-Password": password, + "Auth-Password": urllib.parse.quote(password), "Auth-Port": port } status, code = get_status(protocol, "authentication") diff --git a/towncrier/newsfragments/2837.bugfix b/towncrier/newsfragments/2837.bugfix new file mode 100644 index 00000000..bcbc095a --- /dev/null +++ b/towncrier/newsfragments/2837.bugfix @@ -0,0 +1 @@ +Authentication failed for email clients when the password contained a non latin-1 character. \ No newline at end of file From 008cbdb6b1d9096c8855cb91825d460524a3d9f9 Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Tue, 30 May 2023 14:31:29 +0000 Subject: [PATCH 2/6] Also url encode the password when authentication fails --- core/admin/mailu/internal/nginx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py index dfc24a02..c01152a4 100644 --- a/core/admin/mailu/internal/nginx.py +++ b/core/admin/mailu/internal/nginx.py @@ -120,7 +120,7 @@ def handle_authentication(headers): "Auth-Error-Code": code, "Auth-User": user_email, "Auth-User-Exists": is_valid_user, - "Auth-Password": password, + "Auth-Password": urllib.parse.quote(password), "Auth-Wait": 0 } # Unexpected From 10a3d1eabb98708bcb6344975b2f23a5a57bfa29 Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Tue, 30 May 2023 15:06:32 +0000 Subject: [PATCH 3/6] Get the password from the source. Remove password from response (not needed) --- core/admin/mailu/internal/nginx.py | 2 -- core/admin/mailu/internal/views/auth.py | 11 ++++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py index c01152a4..63dec50a 100644 --- a/core/admin/mailu/internal/nginx.py +++ b/core/admin/mailu/internal/nginx.py @@ -111,7 +111,6 @@ def handle_authentication(headers): "Auth-Server": server, "Auth-User": user_email, "Auth-User-Exists": is_valid_user, - "Auth-Password": urllib.parse.quote(password), "Auth-Port": port } status, code = get_status(protocol, "authentication") @@ -120,7 +119,6 @@ def handle_authentication(headers): "Auth-Error-Code": code, "Auth-User": user_email, "Auth-User-Exists": is_valid_user, - "Auth-Password": urllib.parse.quote(password), "Auth-Wait": 0 } # Unexpected diff --git a/core/admin/mailu/internal/views/auth.py b/core/admin/mailu/internal/views/auth.py index 60e16297..f9dd9ca8 100644 --- a/core/admin/mailu/internal/views/auth.py +++ b/core/admin/mailu/internal/views/auth.py @@ -6,6 +6,7 @@ import flask import flask_login import base64 import sqlalchemy.exc +import urllib @internal.route("/auth/email") def nginx_authentication(): @@ -52,7 +53,15 @@ def nginx_authentication(): if not is_port_25: utils.limiter.exempt_ip_from_ratelimits(client_ip) elif is_valid_user: - utils.limiter.rate_limit_user(username, client_ip, password=response.headers.get('Auth-Password', None)) + raw_password = urllib.parse.unquote(headers["Auth-Pass"]) + password = None + try: + password = raw_password.encode("iso8859-1").decode("utf8") + except: + app.logger.warn(f'Received undecodable password from nginx: {raw_password!r}') + utils.limiter.rate_limit_user(username, client_ip, password=None) + else: + utils.limiter.rate_limit_user(username, client_ip, password=password) elif not is_from_webmail: utils.limiter.rate_limit_ip(client_ip, username) return response From 8c206e8a9bde5011d0d9053b98e30550dc227e94 Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Wed, 31 May 2023 09:08:03 +0000 Subject: [PATCH 4/6] Retrieve raw password on the correct location --- core/admin/mailu/internal/views/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/internal/views/auth.py b/core/admin/mailu/internal/views/auth.py index f9dd9ca8..a123b8b7 100644 --- a/core/admin/mailu/internal/views/auth.py +++ b/core/admin/mailu/internal/views/auth.py @@ -31,6 +31,7 @@ def nginx_authentication(): if int(flask.request.headers['Auth-Login-Attempt']) < 10: response.headers['Auth-Wait'] = '3' return response + raw_password = urllib.parse.unquote(headers["Auth-Pass"]) headers = nginx.handle_authentication(flask.request.headers) response = flask.Response() for key, value in headers.items(): @@ -53,7 +54,6 @@ def nginx_authentication(): if not is_port_25: utils.limiter.exempt_ip_from_ratelimits(client_ip) elif is_valid_user: - raw_password = urllib.parse.unquote(headers["Auth-Pass"]) password = None try: password = raw_password.encode("iso8859-1").decode("utf8") From 9ad7cea5159abda579fb351da7348d09b39dce0c Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Wed, 31 May 2023 09:17:59 +0000 Subject: [PATCH 5/6] Update 05_connectivity test to use UTF8 password. --- tests/compose/core/00_create_users.sh | 1 + tests/compose/core/05_connectivity.py | 22 +++++++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/tests/compose/core/00_create_users.sh b/tests/compose/core/00_create_users.sh index 26461966..142c3cb3 100755 --- a/tests/compose/core/00_create_users.sh +++ b/tests/compose/core/00_create_users.sh @@ -8,4 +8,5 @@ docker compose -f tests/compose/core/docker-compose.yml exec -T admin flask mail docker compose -f tests/compose/core/docker-compose.yml exec -T admin flask mailu admin admin mailu.io 'password' --mode=update || exit 1 docker compose -f tests/compose/core/docker-compose.yml exec -T admin flask mailu user user mailu.io 'password' || exit 1 docker compose -f tests/compose/core/docker-compose.yml exec -T admin flask mailu user 'user/with/slash' mailu.io 'password' || exit 1 +docker compose -f tests/compose/core/docker-compose.yml exec -T admin flask mailu user 'user_UTF8' mailu.io 'password€' || exit 1 echo "User testing successful!" diff --git a/tests/compose/core/05_connectivity.py b/tests/compose/core/05_connectivity.py index cee02cf6..5cc12069 100755 --- a/tests/compose/core/05_connectivity.py +++ b/tests/compose/core/05_connectivity.py @@ -7,25 +7,31 @@ import sys import managesieve SERVER='localhost' -USERNAME='user@mailu.io' -PASSWORD='password' +USERNAME='user_UTF8@mailu.io' +PASSWORD='password€' +#https://github.com/python/cpython/issues/73936 +#SMTPlib does not support UTF8 passwords. +USERNAME_ASCII='user@mailu.io' +PASSWORD_ASCII='password' + def test_imap(server, username, password): + auth = lambda data : f'\x00{username}\x00{password}' print(f'Authenticating to imaps://{username}:{password}@{server}:993/') with imaplib.IMAP4_SSL(server) as conn: - conn.login(username, password) + conn.authenticate('PLAIN', auth) conn.noop() print('OK') print(f'Authenticating to imaps://{username}:{password}@{server}:143/') with imaplib.IMAP4(server) as conn: conn.starttls() - conn.login(username, password) + conn.authenticate('PLAIN', auth) conn.noop() print('OK') print(f'Authenticating to imap://{username}:{password}@{server}:143/') try: with imaplib.IMAP4(server) as conn: - conn.login(username, password) + conn.authenticate('PLAIN', auth) print(f'Authenticating to imap://{username}:{password}@{server}:143/ worked without STARTTLS!') sys.exit(102) except imaplib.IMAP4.error: @@ -121,7 +127,7 @@ def test_managesieve(server, username, password): if m.login('', username, password) != 'OK': print(f'Authenticating to sieve://{username}:{password}@{server}:4190/ has failed!') sys.exit(109) - + if m.listscripts()[0] != 'OK': print(f'Listing scripts failed!') sys.exit(110) @@ -130,5 +136,7 @@ def test_managesieve(server, username, password): if __name__ == '__main__': test_imap(SERVER, USERNAME, PASSWORD) test_pop3(SERVER, USERNAME, PASSWORD) - test_SMTP(SERVER, USERNAME, PASSWORD) + test_SMTP(SERVER, USERNAME_ASCII, PASSWORD_ASCII) test_managesieve(SERVER, USERNAME, PASSWORD) +#https://github.com/python/cpython/issues/73936 +#SMTPlib does not support UTF8 passwords. \ No newline at end of file From 22edc15de225d35460bcb3b447a70a089b4705fd Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 31 May 2023 11:36:28 +0200 Subject: [PATCH 6/6] Update core/admin/mailu/internal/views/auth.py --- core/admin/mailu/internal/views/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/internal/views/auth.py b/core/admin/mailu/internal/views/auth.py index a123b8b7..51ffda9f 100644 --- a/core/admin/mailu/internal/views/auth.py +++ b/core/admin/mailu/internal/views/auth.py @@ -58,7 +58,7 @@ def nginx_authentication(): try: password = raw_password.encode("iso8859-1").decode("utf8") except: - app.logger.warn(f'Received undecodable password from nginx: {raw_password!r}') + app.logger.warn(f'Received undecodable password for {username} from nginx: {raw_password!r}') utils.limiter.rate_limit_user(username, client_ip, password=None) else: utils.limiter.rate_limit_user(username, client_ip, password=password)