mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-31 03:53:50 +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
						Alex Vandiver
					
				
			
			
				
	
			
			
			
						parent
						
							720d16e809
						
					
				
				
					commit
					551b387164
				
			| @@ -143,9 +143,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( | ||||||
| @@ -155,7 +155,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"), | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -773,7 +773,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.assertEqual(len(queries), 70) |         self.assertEqual(len(queries), 72) | ||||||
|  |  | ||||||
|         # 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 | ||||||
| @@ -1876,8 +1876,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 | ||||||
| @@ -1897,8 +1896,17 @@ so we didn't send them an invitation. We did send invitations to everyone else!" | |||||||
|         conf.date_sent -= datetime.timedelta(weeks=3) |         conf.date_sent -= datetime.timedelta(weeks=3) | ||||||
|         conf.save() |         conf.save() | ||||||
|  |  | ||||||
|         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 | ||||||
| @@ -1970,9 +1978,9 @@ so we didn't send them an invitation. We did send invitations to everyone else!" | |||||||
|         response = self.client_post( |         response = self.client_post( | ||||||
|             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, 200) |         self.assertEqual(response.status_code, 404) | ||||||
|         self.assert_in_success_response( |         self.assert_in_response( | ||||||
|             ["The registration link has expired or is not valid."], 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] | ||||||
| @@ -3569,10 +3577,8 @@ class UserSignUpTest(InviteUserBase): | |||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
|         # Error page should be displayed |         # Error page should be displayed | ||||||
|         self.assert_in_success_response( |         self.assert_in_response("The registration link has expired or is not valid.", result) | ||||||
|             ["The registration link has expired or is not valid."], result |         self.assertEqual(result.status_code, 404) | ||||||
|         ) |  | ||||||
|         self.assertEqual(result.status_code, 200) |  | ||||||
|  |  | ||||||
|     def test_signup_with_multiple_default_stream_groups(self) -> None: |     def test_signup_with_multiple_default_stream_groups(self) -> None: | ||||||
|         # Check if user is subscribed to the streams of default |         # Check if user is subscribed to the streams of default | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import logging | import logging | ||||||
| import urllib | import urllib | ||||||
| from typing import Dict, List, Optional | from typing import Dict, List, Optional, Union | ||||||
| from urllib.parse import urlencode | from urllib.parse import urlencode | ||||||
|  |  | ||||||
| import pytz | import pytz | ||||||
| @@ -87,8 +87,34 @@ from zproject.backends import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def check_prereg_key_and_redirect(request: HttpRequest, confirmation_key: str) -> HttpResponse: | def get_prereg_key_and_redirect(request: HttpRequest, confirmation_key: str) -> 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": request.GET.get("full_name", None)}, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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, | ||||||
| @@ -107,27 +133,19 @@ def check_prereg_key_and_redirect(request: HttpRequest, confirmation_key: str) - | |||||||
|     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": request.GET.get("full_name", None)}, |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @require_post | @require_post | ||||||
| def accounts_register(request: HttpRequest) -> HttpResponse: | def accounts_register(request: HttpRequest) -> HttpResponse: | ||||||
|     try: |     key = request.POST.get("key", default="") | ||||||
|         key = request.POST.get("key", default="") |  | ||||||
|         confirmation = Confirmation.objects.get(confirmation_key=key) |  | ||||||
|     except Confirmation.DoesNotExist: |  | ||||||
|         return render(request, "zerver/confirmation_link_expired_error.html") |  | ||||||
|  |  | ||||||
|     prereg_user = confirmation.content_object |     key_check_result = check_prereg_key(request, key) | ||||||
|     if prereg_user.status == confirmation_settings.STATUS_REVOKED: |     if isinstance(key_check_result, HttpResponse): | ||||||
|         return render(request, "zerver/confirmation_link_expired_error.html") |         return key_check_result | ||||||
|  |  | ||||||
|  |     prereg_user = key_check_result.content_object | ||||||
|  |     assert prereg_user is not None | ||||||
|     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 | ||||||
|   | |||||||
| @@ -126,9 +126,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 ( | ||||||
| @@ -570,8 +570,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