Skip to content
Permalink
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mateuszmandera authored and alexmv committed Feb 25, 2022
1 parent d5db254 commit 8891701
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 9 deletions.
43 changes: 43 additions & 0 deletions zerver/tests/test_auth_backends.py
Expand Up @@ -91,6 +91,7 @@
PreregistrationUser,
Realm,
RealmDomain,
Stream,
UserProfile,
clear_supported_auth_backends_cache,
email_to_username,
Expand Down Expand Up @@ -1582,6 +1583,48 @@ def test_social_auth_registration_using_multiuse_invite(self) -> None:
result, realm, subdomain, email, name, name, self.BACKEND_CLASS.full_name_validated
)

@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
)

def test_social_auth_registration_without_is_signup(self) -> None:
"""If `is_signup` is not set then a new account isn't created"""
email = "newuser@zulip.com"
Expand Down
31 changes: 30 additions & 1 deletion zerver/tests/test_signup.py
Expand Up @@ -11,7 +11,7 @@
from django.contrib.auth.views import PasswordResetConfirmView
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.http import HttpResponse
from django.http import HttpRequest, HttpResponse
from django.test import Client, override_settings
from django.urls import reverse
from django.utils.timezone import now as timezone_now
Expand Down Expand Up @@ -107,6 +107,7 @@
from zerver.views.auth import redirect_and_log_into_subdomain, start_two_factor_auth
from zerver.views.development.registration import confirmation_key
from zerver.views.invite import get_invitee_emails_set
from zerver.views.registration import accounts_home
from zproject.backends import ExternalAuthDataDict, ExternalAuthResult


Expand Down Expand Up @@ -2903,6 +2904,34 @@ def test_multiuse_link_with_specified_streams(self) -> None:
self.check_user_able_to_register(email2, invite_link)
self.check_user_subscribed_only_to_streams(name2, streams)

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)

def test_create_multiuse_link_api_call(self) -> None:
self.login("iago")

Expand Down
16 changes: 10 additions & 6 deletions zerver/views/auth.py
Expand Up @@ -181,8 +181,14 @@ def maybe_send_to_registration(
request.session, "registration_desktop_flow_otp", desktop_flow_otp, expiry_seconds=3600
)

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

multiuse_obj: Optional[MultiuseInvite] = None
realm: Optional[Realm] = None
from_multiuse_invite = False
if multiuse_object_key:
from_multiuse_invite = True
Expand All @@ -192,13 +198,11 @@ def maybe_send_to_registration(
return render_confirmation_key_error(request, exception)

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

invited_as = multiuse_obj.invited_as
else:
try:
realm = get_realm(get_subdomain(request))
except Realm.DoesNotExist:
pass
invited_as = PreregistrationUser.INVITE_AS["MEMBER"]

form = HomepageForm({"email": email}, realm=realm, from_multiuse_invite=from_multiuse_invite)
Expand Down
10 changes: 8 additions & 2 deletions zerver/views/registration.py
Expand Up @@ -676,7 +676,11 @@ def accounts_home(
invited_as = None

if multiuse_object:
realm = multiuse_object.realm
# 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

streams_to_subscribe = multiuse_object.streams.all()
from_multiuse_invite = True
invited_as = multiuse_object.invited_as
Expand Down Expand Up @@ -726,12 +730,14 @@ def accounts_home(


def accounts_home_from_multiuse_invite(request: HttpRequest, confirmation_key: str) -> HttpResponse:
realm = get_realm_from_request(request)
multiuse_object = None
try:
multiuse_object = get_object_from_key(confirmation_key, [Confirmation.MULTIUSE_INVITE])
if realm != multiuse_object.realm:
return render(request, "confirmation/link_does_not_exist.html", status=404)
# Required for OAuth 2
except ConfirmationKeyException as exception:
realm = get_realm_from_request(request)
if realm is None or realm.invite_required:
return render_confirmation_key_error(request, exception)
return accounts_home(
Expand Down

0 comments on commit 8891701

Please sign in to comment.