diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py index ebd677d0..daef8b9e 100644 --- a/core/admin/mailu/internal/nginx.py +++ b/core/admin/mailu/internal/nginx.py @@ -91,20 +91,14 @@ def handle_authentication(headers): # Authenticated user elif method in ['plain', 'login']: is_valid_user = False - # According to RFC2616 section 3.7.1 and PEP 3333, HTTP headers should - # be ASCII and are generally considered ISO8859-1. However when passing - # the password, nginx does not transcode the input UTF string, thus - # we need to manually decode. - 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") + user_email = urllib.parse.unquote(headers["Auth-User"]) + password = urllib.parse.unquote(headers["Auth-Pass"]) ip = urllib.parse.unquote(headers["Client-Ip"]) except: - app.logger.warn(f'Received undecodable user/password from nginx: {raw_user_email!r}/{raw_password!r}') + app.logger.warn(f'Received undecodable user/password from front: {headers.get("Auth-User", "")!r}') else: try: user = models.User.query.get(user_email) if '@' in user_email else None diff --git a/core/admin/mailu/internal/views/auth.py b/core/admin/mailu/internal/views/auth.py index 4aa31407..c74bcc9e 100644 --- a/core/admin/mailu/internal/views/auth.py +++ b/core/admin/mailu/internal/views/auth.py @@ -29,7 +29,6 @@ def nginx_authentication(): response.headers['Auth-Status'] = status response.headers['Auth-Error-Code'] = code return response - raw_password = urllib.parse.unquote(headers['Auth-Pass']) if 'Auth-Pass' in headers else '' headers = nginx.handle_authentication(flask.request.headers) response = flask.Response() for key, value in headers.items(): @@ -50,14 +49,8 @@ def nginx_authentication(): if not is_port_25: utils.limiter.exempt_ip_from_ratelimits(client_ip) elif is_valid_user: - password = None - try: - password = raw_password.encode("iso8859-1").decode("utf8") - except: - 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) + password = urllib.parse.unquote(headers.get('Auth-Pass', '')) + 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 diff --git a/core/nginx/dovecot/login.lua b/core/nginx/dovecot/login.lua index d24de149..442833f1 100644 --- a/core/nginx/dovecot/login.lua +++ b/core/nginx/dovecot/login.lua @@ -10,18 +10,28 @@ local http_client = dovecot.http.client { max_attempts = 3; } +-- on the other end we use urllib.parse.unquote() +function urlEncode(str) + return str:gsub("[^%w_.-~]", function(c) + return string.format("%%%02X", string.byte(c)) + end) +end + function auth_passdb_lookup(req) local auth_request = http_client:request { url = "http://{{ ADMIN_ADDRESS }}:8080/internal/auth/email"; } auth_request:add_header('Auth-Port', req.local_port) - auth_request:add_header('Auth-User', req.user) + local user = urlEncode(req.user) + auth_request:add_header('Auth-User', user) if req.password ~= nil then - auth_request:add_header('Auth-Pass', req.password) + local password = urlEncode(req.password) + auth_request:add_header('Auth-Pass', password) end auth_request:add_header('Auth-Protocol', req.service) - auth_request:add_header('Client-IP', req.remote_ip) + local client_ip = urlEncode(req.remote_ip) + auth_request:add_header('Client-Ip', client_ip) auth_request:add_header('Client-Port', req.remote_port) auth_request:add_header('Auth-SSL', req.secured) auth_request:add_header('Auth-Method', req.mechanism) diff --git a/tests/compose/core/00_create_users.sh b/tests/compose/core/00_create_users.sh index 142c3cb3..0bed6baf 100755 --- a/tests/compose/core/00_create_users.sh +++ b/tests/compose/core/00_create_users.sh @@ -8,5 +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 +docker compose -f tests/compose/core/docker-compose.yml exec -T admin flask mailu user 'user_UTF8' mailu.io 'pa…ss%e9word€' || exit 1 echo "User testing successful!" diff --git a/tests/compose/core/05_connectivity.py b/tests/compose/core/05_connectivity.py index 5cc12069..44bb4553 100755 --- a/tests/compose/core/05_connectivity.py +++ b/tests/compose/core/05_connectivity.py @@ -8,7 +8,7 @@ import managesieve SERVER='localhost' USERNAME='user_UTF8@mailu.io' -PASSWORD='password€' +PASSWORD='pa…ss%e9word€' #https://github.com/python/cpython/issues/73936 #SMTPlib does not support UTF8 passwords. USERNAME_ASCII='user@mailu.io' @@ -139,4 +139,4 @@ if __name__ == '__main__': 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 +#SMTPlib does not support UTF8 passwords. diff --git a/towncrier/newsfragments/3364.bugfix b/towncrier/newsfragments/3364.bugfix new file mode 100644 index 00000000..10e56cf3 --- /dev/null +++ b/towncrier/newsfragments/3364.bugfix @@ -0,0 +1 @@ +Fix a bug preventing percent characters from being used in passwords