CVE-2022-21706 (Python, published 2022-02-26T00:15Z)
Description: value: Zulip is an open-source team collaboration tool with topic-based threading. Zulip Server version 2.0.0 and above are vulnerable to insufficient access control with multi-use invitations. A Zulip Server deployment which hosts multiple organizations is vulnerable to an attack where an invitation created in one organization (potentially as a role with elevated permissions) can be used to join any other organization. This bypasses any restrictions on required domains on users' email addresses, may be used to gain access to organizations which are only accessible by invitation, and may be used to gain access with elevated privileges. This issue has been patched in release 4.10. There are no known workarounds for this issue. ### Patches _Has the problem been patched? What versions should users upgrade to?_ ### Workarounds _Is there a way for users to fix or remediate the vulnerability without upgrading?_ ### References _Are there any links users can visit to find out more?_ ### For more information If you have any questions or comments about this advisory, you can discuss them on the [developer community Zulip server](https://zulip.com/developer-community/), or email the [Zulip security team](mailto:security@zulip.com).
Severity: HIGH (CVSS v3 base score: 9.8)
Weakness: NVD-CWE-Other \u2014 Other
NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
Repository: https://github.com/zulip/zulip
Files changed:
zerver/tests/test_auth_backends.py
zerver/tests/test_signup.py
zerver/views/auth.py
zerver/views/registration.py
Commit message: CVE-2022-21706: Prevent use of multiuse invites to join other orgs.

The codepaths for joining an organization via a multi-use invitation
(accounts_home_from_multiuse_invite and maybe_send_to_registration)
weren't validating whether
the organization the invite was generated for matches the organization
the user attempts to join - potentially allowing an attacker with access
to organization A to generate a multi-use invite and use it to join
organization B within the same deployment, that they shouldn't have
access to.


Vulnerable code:
from django.http import HttpResponse
    realm: Optional[Realm] = None
        realm = multiuse_obj.realm
        try:
            realm = get_realm(get_subdomain(request))
        except Realm.DoesNotExist:
            pass
        realm = multiuse_object.realm
        realm = get_realm_from_request(request)

Fixed code:
    Stream,
    @override_settings(TERMS_OF_SERVICE_VERSION=None)
    def test_social_auth_registration_using_multiuse_invite_realm_validation(self) -> None:
        """If the user doesn't exist yet, social auth can be used to register an account"""
        email = "newuser@zulip.com"
        name = "Full Name"
        subdomain = "zulip"
        realm = get_realm("zulip")
        realm.invite_required = True
        realm.save()

        streams: List[Stream] = []

        # Generate an invitation for a different realm than the one we'll attempt to join:
        lear_realm = get_realm("lear")
        multiuse_obj = MultiuseInvite.objects.create(
            realm=lear_realm, referred_by=UserProfile.objects.filter(realm=lear_realm).first()
        )
        multiuse_obj.streams.set(streams)
        validity_in_days = 2
        create_confirmation_link(
            multiuse_obj, Confirmation.MULTIUSE_INVITE, validity_in_days=validity_in_days
        )
        multiuse_confirmation = Confirmation.objects.all().last()
        assert multiuse_confirmation is not None
        multiuse_object_key = multiuse_confirmation.confirmation_key
        account_data_dict = self.get_account_data_dict(email=email, name=name)

        # Now we try to use the invitation for the lear realm to join the zulip realm,
        # which should fail.
        result = self.social_auth_test(
            account_data_dict,
            subdomain=subdomain,
            is_signup=True,
            expect_choose_email_screen=True,
            multiuse_object_key=multiuse_object_key,
        )

        result = self.client_get(result.url)
        self.assert_in_response(
            "Whoops. We couldn't find your confirmation link in the system.", result
        )

from django.http import HttpRequest, HttpResponse
from zerver.views.registration import accounts_home
    def test_multiuse_link_different_realms(self) -> None:
        """
        Verify that an invitation generated for one realm can't be used
        to join another.
        """
        lear_realm = get_realm("lear")
        self.realm = lear_realm
        invite_link = self.generate_multiuse_invite_link(streams=[])
        key = invite_link.split("/")[-2]

        result = self.client_get(f"/join/{key}/", subdomain="zulip")
        self.assertEqual(result.status_code, 404)
        self.assert_in_response(
            "Whoops. We couldn't find your confirmation link in the system.", result
        )

        # Now we want to test the accounts_home function, which can't be used
        # for the multiuse invite case via an HTTP request, but is still supposed
        # to do its own verification that the realms match as a hardening measure
        # against a caller that fails to do that.
        request = HttpRequest()
        confirmation = Confirmation.objects.get(confirmation_key=key)
        multiuse_object = confirmation.content_object
        with patch(
            "zerver.views.registration.get_subdomain", return_value="zulip"
        ), self.assertRaises(AssertionError):
            accounts_home(request, multiuse_object=multiuse_object)

    try:
        # TODO: This should use get_realm_from_request, but a bunch of tests
        # rely on mocking get_subdomain here, so they'll need to be tweaked first.
        realm: Optional[Realm] = get_realm(get_subdomain(request))
    except Realm.DoesNotExist:
        realm = None

        if realm != multiuse_obj.realm:
            return render(request, "confirmation/link_does_not_exist.html", status=404)

        # multiuse_object's realm should have been validated by the caller,
        # so this code shouldn't be reachable with a multiuse_object which
        # has its realm mismatching the realm of the request.
        assert realm == multiuse_object.realm

    realm = get_realm_from_request(request)
        if realm != multiuse_object.realm:
            return render(request, "confirmation/link_does_not_exist.html", status=404)