CVE-2021-43791 (published 2021-12-02T01:15Z)

Zulip is an open source group chat application that combines real-time chat with threaded conversations. In affected versions expiration dates on the confirmation objects associated with email invitations were not enforced properly in the new account registration flow. A confirmation link takes a user to the check_prereg_key_and_redirect endpoint, before getting redirected to POST to /accounts/register/. The problem was that validation was happening in the check_prereg_key_and_redirect part and not in /accounts/register/ - meaning that one could submit an expired confirmation key and be able to register. The issue is fixed in Zulip 4.8. There are no known workarounds and users are advised to upgrade as soon as possible.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:N/I:P/A:N)
CVSS v3 base score: 5.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N)

Weakness information:
cwe_id: CWE-613
cwe_name: Insufficient Session Expiration
description: According to WASC, "Insufficient Session Expiration is when a web site permits an attacker to reuse old session credentials or session IDs for authorization."
extended_description: 
            
            
            
         
         

Fix information:
repo_url: https://github.com/zulip/zulip
commit_details: repo_url: https://github.com/zulip/zulip
author: Mateusz Mandera
author_date: 2021-11-29 16:20:59+01:00
msg: CVE-2021-43791: Validate confirmation keys in /accounts/register/ codepath.

A confirmation link takes a user to the check_prereg_key_and_redirect
endpoint, before getting redirected to POST to /accounts/register/. The
problem was that validation was happening in the check_prereg_key_and_redirect
part and not in /accounts/register/ - meaning that one could submit an
expired confirmation key and be able to register.

We fix this by moving validation into /accouts/register/.
repository: repo_url: https://github.com/zulip/zulip
repo_name: zulip/zulip
description: Zulip server and web application. Open-source team chat that helps teams stay productive and focused.
repo_language: Python
owner: zulip
file_changes: filename: models.py
change_type: ModificationType.MODIFY
diff: @@ -170,9 +170,9 @@ class ConfirmationType:
 
 
 _properties = {
-    Confirmation.USER_REGISTRATION: ConfirmationType("check_prereg_key_and_redirect"),
+    Confirmation.USER_REGISTRATION: ConfirmationType("get_prereg_key_and_redirect"),
     Confirmation.INVITATION: ConfirmationType(
-        "check_prereg_key_and_redirect", validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS
+        "get_prereg_key_and_redirect", validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS
     ),
     Confirmation.EMAIL_CHANGE: ConfirmationType("confirm_email_change"),
     Confirmation.UNSUBSCRIBE: ConfirmationType(
@@ -182,7 +182,7 @@ _properties = {
     Confirmation.MULTIUSE_INVITE: ConfirmationType(
         "join", validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS
     ),
-    Confirmation.REALM_CREATION: ConfirmationType("check_prereg_key_and_redirect"),
+    Confirmation.REALM_CREATION: ConfirmationType("get_prereg_key_and_redirect"),
     Confirmation.REALM_REACTIVATION: ConfirmationType("realm_reactivation"),
 }
 

programming_language: Python
filename: test_signup.py
change_type: ModificationType.MODIFY
diff: @@ -855,7 +855,7 @@ class LoginTest(ZulipTestCase):
         with queries_captured() as queries, cache_tries_captured() as cache_tries:
             self.register(self.nonreg_email("test"), "test")
         # Ensure the number of queries we make is not O(streams)
-        self.assert_length(queries, 89)
+        self.assert_length(queries, 91)
 
         # We can probably avoid a couple cache hits here, but there doesn't
         # seem to be any O(N) behavior.  Some of the cache hits are related
@@ -2014,8 +2014,7 @@ so we didn't send them an invitation. We did send invitations to everyone else!"
         # Verify that using the wrong type doesn't work in the main confirm code path
         email_change_url = create_confirmation_link(prereg_user, Confirmation.EMAIL_CHANGE)
         email_change_key = email_change_url.split("/")[-1]
-        url = "/accounts/do_confirm/" + email_change_key
-        result = self.client_get(url)
+        result = self.client_post("/accounts/register/", {"key": email_change_key})
         self.assertEqual(result.status_code, 404)
         self.assert_in_response(
             "Whoops. We couldn't find your confirmation link in the system.", result
@@ -2032,8 +2031,17 @@ so we didn't send them an invitation. We did send invitations to everyone else!"
         with patch("confirmation.models.timezone_now", return_value=date_sent):
             url = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
 
-        target_url = "/" + url.split("/", 3)[3]
-        result = self.client_get(target_url)
+        key = url.split("/")[-1]
+        confirmation_link_path = "/" + url.split("/", 3)[3]
+        # Both the confirmation link and submitting the key to the registration endpoint
+        # directly will return the appropriate error.
+        result = self.client_get(confirmation_link_path)
+        self.assertEqual(result.status_code, 404)
+        self.assert_in_response(
+            "Whoops. The confirmation link has expired or been deactivated.", result
+        )
+
+        result = self.client_post("/accounts/register/", {"key": key})
         self.assertEqual(result.status_code, 404)
         self.assert_in_response(
             "Whoops. The confirmation link has expired or been deactivated.", result
@@ -2124,7 +2132,9 @@ so we didn't send them an invitation. We did send invitations to everyone else!"
             url, {"key": registration_key, "from_confirmation": 1, "full_nme": "alice"}
         )
         self.assertEqual(response.status_code, 404)
-        self.assert_in_response("The registration link has expired or is not valid.", response)
+        self.assert_in_response(
+            "Whoops. We couldn't find your confirmation link in the system.", response
+        )
 
         registration_key = confirmation_link.split("/")[-1]
         response = self.client_post(

programming_language: Python
filename: registration.py
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,6 @@
 import logging
 import urllib
-from typing import Any, Dict, List, Optional
+from typing import Any, Dict, List, Optional, Union
 from urllib.parse import urlencode
 
 from django.conf import settings
@@ -94,10 +94,36 @@ if settings.BILLING_ENABLED:
 
 
 @has_request_variables
-def check_prereg_key_and_redirect(
+def get_prereg_key_and_redirect(
     request: HttpRequest, confirmation_key: str, full_name: Optional[str] = REQ(default=None)
 ) -> HttpResponse:
-    confirmation = Confirmation.objects.filter(confirmation_key=confirmation_key).first()
+    key_check_result = check_prereg_key(request, confirmation_key)
+    if isinstance(key_check_result, HttpResponse):
+        return key_check_result
+    # confirm_preregistrationuser.html just extracts the confirmation_key
+    # (and GET parameters) and redirects to /accounts/register, so that the
+    # user can enter their information on a cleaner URL.
+    return render(
+        request,
+        "confirmation/confirm_preregistrationuser.html",
+        context={"key": confirmation_key, "full_name": full_name},
+    )
+
+
+def check_prereg_key(
+    request: HttpRequest, confirmation_key: str
+) -> Union[Confirmation, HttpResponse]:
+    """
+    Checks if the Confirmation key is valid, returning the Confirmation object in case of success
+    and an appropriate error page otherwise.
+    """
+    try:
+        confirmation: Optional[Confirmation] = Confirmation.objects.get(
+            confirmation_key=confirmation_key
+        )
+    except Confirmation.DoesNotExist:
+        confirmation = None
+
     if confirmation is None or confirmation.type not in [
         Confirmation.USER_REGISTRATION,
         Confirmation.INVITATION,
@@ -117,14 +143,7 @@ def check_prereg_key_and_redirect(
     except ConfirmationKeyException as exception:
         return render_confirmation_key_error(request, exception)
 
-    # confirm_preregistrationuser.html just extracts the confirmation_key
-    # (and GET parameters) and redirects to /accounts/register, so that the
-    # user can enter their information on a cleaner URL.
-    return render(
-        request,
-        "confirmation/confirm_preregistrationuser.html",
-        context={"key": confirmation_key, "full_name": full_name},
-    )
+    return confirmation
 
 
 @require_post
@@ -139,15 +158,12 @@ def accounts_register(
         default=None, converter=to_converted_or_fallback(to_non_negative_int, None)
     ),
 ) -> HttpResponse:
-    try:
-        confirmation = Confirmation.objects.get(confirmation_key=key)
-    except Confirmation.DoesNotExist:
-        return render(request, "zerver/confirmation_link_expired_error.html", status=404)
+    key_check_result = check_prereg_key(request, key)
+    if isinstance(key_check_result, HttpResponse):
+        return key_check_result
 
-    prereg_user = confirmation.content_object
+    prereg_user = key_check_result.content_object
     assert prereg_user is not None
-    if prereg_user.status == confirmation_settings.STATUS_REVOKED:
-        return render(request, "zerver/confirmation_link_expired_error.html", status=404)
     email = prereg_user.email
     realm_creation = prereg_user.realm_creation
     password_required = prereg_user.password_required

programming_language: Python
filename: urls.py
change_type: ModificationType.MODIFY
diff: @@ -128,9 +128,9 @@ from zerver.views.registration import (
     accounts_home,
     accounts_home_from_multiuse_invite,
     accounts_register,
-    check_prereg_key_and_redirect,
     create_realm,
     find_account,
+    get_prereg_key_and_redirect,
     realm_redirect,
 )
 from zerver.views.report import (
@@ -559,8 +559,8 @@ i18n_urls = [
     path("accounts/register/", accounts_register, name="accounts_register"),
     path(
         "accounts/do_confirm/<confirmation_key>",
-        check_prereg_key_and_redirect,
-        name="check_prereg_key_and_redirect",
+        get_prereg_key_and_redirect,
+        name="get_prereg_key_and_redirect",
     ),
     path(
         "accounts/confirm_new_email/<confirmation_key>",

programming_language: Python