mirror of
https://github.com/zulip/zulip.git
synced 2025-11-04 14:03:30 +00:00
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/.
This commit is contained in:
committed by
Alex Vandiver
parent
a1cd660147
commit
a014ef75a3
@@ -170,9 +170,9 @@ class ConfirmationType:
|
|||||||
|
|
||||||
|
|
||||||
_properties = {
|
_properties = {
|
||||||
Confirmation.USER_REGISTRATION: ConfirmationType("check_prereg_key_and_redirect"),
|
Confirmation.USER_REGISTRATION: ConfirmationType("get_prereg_key_and_redirect"),
|
||||||
Confirmation.INVITATION: ConfirmationType(
|
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.EMAIL_CHANGE: ConfirmationType("confirm_email_change"),
|
||||||
Confirmation.UNSUBSCRIBE: ConfirmationType(
|
Confirmation.UNSUBSCRIBE: ConfirmationType(
|
||||||
@@ -182,7 +182,7 @@ _properties = {
|
|||||||
Confirmation.MULTIUSE_INVITE: ConfirmationType(
|
Confirmation.MULTIUSE_INVITE: ConfirmationType(
|
||||||
"join", validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS
|
"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"),
|
Confirmation.REALM_REACTIVATION: ConfirmationType("realm_reactivation"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -855,7 +855,7 @@ class LoginTest(ZulipTestCase):
|
|||||||
with queries_captured() as queries, cache_tries_captured() as cache_tries:
|
with queries_captured() as queries, cache_tries_captured() as cache_tries:
|
||||||
self.register(self.nonreg_email("test"), "test")
|
self.register(self.nonreg_email("test"), "test")
|
||||||
# Ensure the number of queries we make is not O(streams)
|
# 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
|
# 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
|
# 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
|
# 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_url = create_confirmation_link(prereg_user, Confirmation.EMAIL_CHANGE)
|
||||||
email_change_key = email_change_url.split("/")[-1]
|
email_change_key = email_change_url.split("/")[-1]
|
||||||
url = "/accounts/do_confirm/" + email_change_key
|
result = self.client_post("/accounts/register/", {"key": email_change_key})
|
||||||
result = self.client_get(url)
|
|
||||||
self.assertEqual(result.status_code, 404)
|
self.assertEqual(result.status_code, 404)
|
||||||
self.assert_in_response(
|
self.assert_in_response(
|
||||||
"Whoops. We couldn't find your confirmation link in the system.", result
|
"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):
|
with patch("confirmation.models.timezone_now", return_value=date_sent):
|
||||||
url = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
|
url = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
|
||||||
|
|
||||||
target_url = "/" + url.split("/", 3)[3]
|
key = url.split("/")[-1]
|
||||||
result = self.client_get(target_url)
|
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.assertEqual(result.status_code, 404)
|
||||||
self.assert_in_response(
|
self.assert_in_response(
|
||||||
"Whoops. The confirmation link has expired or been deactivated.", result
|
"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"}
|
url, {"key": registration_key, "from_confirmation": 1, "full_nme": "alice"}
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 404)
|
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]
|
registration_key = confirmation_link.split("/")[-1]
|
||||||
response = self.client_post(
|
response = self.client_post(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import urllib
|
import urllib
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional, Union
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -94,10 +94,36 @@ if settings.BILLING_ENABLED:
|
|||||||
|
|
||||||
|
|
||||||
@has_request_variables
|
@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)
|
request: HttpRequest, confirmation_key: str, full_name: Optional[str] = REQ(default=None)
|
||||||
) -> HttpResponse:
|
) -> 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 [
|
if confirmation is None or confirmation.type not in [
|
||||||
Confirmation.USER_REGISTRATION,
|
Confirmation.USER_REGISTRATION,
|
||||||
Confirmation.INVITATION,
|
Confirmation.INVITATION,
|
||||||
@@ -117,14 +143,7 @@ def check_prereg_key_and_redirect(
|
|||||||
except ConfirmationKeyException as exception:
|
except ConfirmationKeyException as exception:
|
||||||
return render_confirmation_key_error(request, exception)
|
return render_confirmation_key_error(request, exception)
|
||||||
|
|
||||||
# confirm_preregistrationuser.html just extracts the confirmation_key
|
return confirmation
|
||||||
# (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},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@require_post
|
@require_post
|
||||||
@@ -139,15 +158,12 @@ def accounts_register(
|
|||||||
default=None, converter=to_converted_or_fallback(to_non_negative_int, None)
|
default=None, converter=to_converted_or_fallback(to_non_negative_int, None)
|
||||||
),
|
),
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
try:
|
key_check_result = check_prereg_key(request, key)
|
||||||
confirmation = Confirmation.objects.get(confirmation_key=key)
|
if isinstance(key_check_result, HttpResponse):
|
||||||
except Confirmation.DoesNotExist:
|
return key_check_result
|
||||||
return render(request, "zerver/confirmation_link_expired_error.html", status=404)
|
|
||||||
|
|
||||||
prereg_user = confirmation.content_object
|
prereg_user = key_check_result.content_object
|
||||||
assert prereg_user is not None
|
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
|
email = prereg_user.email
|
||||||
realm_creation = prereg_user.realm_creation
|
realm_creation = prereg_user.realm_creation
|
||||||
password_required = prereg_user.password_required
|
password_required = prereg_user.password_required
|
||||||
|
|||||||
@@ -128,9 +128,9 @@ from zerver.views.registration import (
|
|||||||
accounts_home,
|
accounts_home,
|
||||||
accounts_home_from_multiuse_invite,
|
accounts_home_from_multiuse_invite,
|
||||||
accounts_register,
|
accounts_register,
|
||||||
check_prereg_key_and_redirect,
|
|
||||||
create_realm,
|
create_realm,
|
||||||
find_account,
|
find_account,
|
||||||
|
get_prereg_key_and_redirect,
|
||||||
realm_redirect,
|
realm_redirect,
|
||||||
)
|
)
|
||||||
from zerver.views.report import (
|
from zerver.views.report import (
|
||||||
@@ -559,8 +559,8 @@ i18n_urls = [
|
|||||||
path("accounts/register/", accounts_register, name="accounts_register"),
|
path("accounts/register/", accounts_register, name="accounts_register"),
|
||||||
path(
|
path(
|
||||||
"accounts/do_confirm/<confirmation_key>",
|
"accounts/do_confirm/<confirmation_key>",
|
||||||
check_prereg_key_and_redirect,
|
get_prereg_key_and_redirect,
|
||||||
name="check_prereg_key_and_redirect",
|
name="get_prereg_key_and_redirect",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"accounts/confirm_new_email/<confirmation_key>",
|
"accounts/confirm_new_email/<confirmation_key>",
|
||||||
|
|||||||
Reference in New Issue
Block a user