Repository: Mailu/Mailu

Issue:
Insufficient input validation in  /internal endpoint
It seems like it is possible to crash a thread of flask by sending an invalid utf-8 username.
As far as i can see, this can not be used to gain access (should be validated).

Here are logs to reproduce this.

[error] 56#56: *1312970 auth http server 127.0.0.1:8000 did not send server or port while in http auth state, client: xx.xx.xx.xx, server: 0.0.0.0:25, login: "xxxxxxxxx\ufffdn@xxxx.xxxx"

GET /internal/auth/email HTTP/1.0" 500 290 "-" "-"

ERROR in app: Exception on /internal/auth/email [GET]
Traceback (most recent call last):
  File "/usr/lib/python3.7/site-packages/flask/app.py", line 2292, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/lib/python3.7/site-packages/flask/app.py", line 1815, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/usr/lib/python3.7/site-packages/flask/app.py", line 1718, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/lib/python3.7/site-packages/flask/_compat.py", line 35, in reraise
    raise value
  File "/usr/lib/python3.7/site-packages/flask/app.py", line 1813, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/lib/python3.7/site-packages/flask/app.py", line 1799, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/usr/lib/python3.7/site-packages/flask_limiter/extension.py", line 544, in __inner
    return obj(*a, **k)
  File "/app/mailu/internal/views/auth.py", line 18, in nginx_authentication
    headers = nginx.handle_authentication(flask.request.headers)
  File "/app/mailu/internal/nginx.py", line 45, in handle_authentication
    user_email = raw_user_email.encode("iso8859-1").decode("utf8")
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xed in position 6: invalid continuation byte

xx.xx.xx.xx - - [xx/xx/xxxx:xx:xx:xx +0000] "GET /internal/auth/email HTTP/1.0" 500 290 "-" "-"



Discussion:
Thank you for reporting.

I confirm this cannot be used to gain access.

Did you encounter this during normal use of Mailu? If not I am in favor of just dropping non-utf8 usernames 
I get this a few times a day. In fact this report is a real case.
I don't want to drop any usernames at all, maybe better errorhandling is a solution? UnicodeDecodeError is a quite explicit exception. catch it and return a 404? 
Yep, probably the best fix :)
Hi There,

The `Mailu`-Project is currently in a bit of a bind! We are short on man-power, and we need to judge if it is possible for us to put in some work on this issue.

To help with that, we are currently trying to find out which issues are actively keeping users from using `Mailu`, which issues have someone who want to work on them — and which issues may be less important. These a less important ones could be discarded for the time being, until the project is in a more stable and regular state once again.

In order for us to better assess this, it would be helpful if you could put a **reaction on this post** (use the :smiley: icon to the top-right).
- 👍️ if you **need this** to be able to use Mailu. Ideally, you’d also be able to test this on your installation, and provide feedback …
- 🎉 if you find it a **nice bonus**, but no deal-breaker
- 🚀 if you want to **work on it yourself**!
We want to keep this **voting open for 2 weeks** from now, so please help out!
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Why stale-bot is targeting this? This is a bug and should be resolved sometime. Just closing does not fix the issue
I get that error quite often too BTW.
Any chance we could address it?

Resolving patch:
diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py
--- a/core/admin/mailu/internal/nginx.py
+++ b/core/admin/mailu/internal/nginx.py
@@ -71,16 +71,6 @@ def handle_authentication(headers):
             }
     # Authenticated user
     elif method == "plain":
-        server, port = get_server(headers["Auth-Protocol"], True)
-        # 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"])
-        user_email = raw_user_email.encode("iso8859-1").decode("utf8")
-        raw_password = urllib.parse.unquote(headers["Auth-Pass"])
-        password = raw_password.encode("iso8859-1").decode("utf8")
-        ip = urllib.parse.unquote(headers["Client-Ip"])
         service_port = int(urllib.parse.unquote(headers["Auth-Port"]))
         if service_port == 25:
             return {
@@ -88,20 +78,33 @@ def handle_authentication(headers):
                 "Auth-Error-Code": "502 5.5.1",
                 "Auth-Wait": 0
             }
-        user = models.User.query.get(user_email)
-        if check_credentials(user, password, ip, protocol):
-            return {
-                "Auth-Status": "OK",
-                "Auth-Server": server,
-                "Auth-Port": port
-            }
+        # 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"])
+        try:
+            user_email = raw_user_email.encode("iso8859-1").decode("utf8")
+            password = raw_password.encode("iso8859-1").decode("utf8")
+        except:
+            app.logger.warn(f'Received undecodable user/password from nginx: {raw_user_email!r}/{raw_password!r}')
         else:
-            status, code = get_status(protocol, "authentication")
-            return {
-                "Auth-Status": status,
-                "Auth-Error-Code": code,
-                "Auth-Wait": 0
-            }
+            user = models.User.query.get(user_email)
+            ip = urllib.parse.unquote(headers["Client-Ip"])
+            if check_credentials(user, password, ip, protocol):
+                server, port = get_server(headers["Auth-Protocol"], True)
+                return {
+                    "Auth-Status": "OK",
+                    "Auth-Server": server,
+                    "Auth-Port": port
+                }
+        status, code = get_status(protocol, "authentication")
+        return {
+            "Auth-Status": status,
+            "Auth-Error-Code": code,
+            "Auth-Wait": 0
+        }
     # Unexpected
     return {}
 


Test patch: