CVE-2021-3967 (published 2022-02-26T23:15Z)

Improper Access Control in GitHub repository zulip/zulip prior to 4.10.

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

Weakness information:
cwe_id: NVD-CWE-Other
cwe_name: Other
description: 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.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/zulip/zulip
commit_details: repo_url: https://github.com/zulip/zulip
author: Mateusz Mandera
author_date: 2022-02-18 21:49:17+01:00
msg: CVE-2021-3967: Only regenerate the API key by authing with the old key.
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: settings_account.js
change_type: ModificationType.MODIFY
diff: @@ -359,8 +359,15 @@ export function set_up() {
         }
 
         $("#regenerate_api_key").on("click", (e) => {
+            const email = page_params.delivery_email;
+            const api_key = $("#api_key_value").text();
+            const authorization_header = "Basic " + btoa(`${email}:${api_key}`);
+
             channel.post({
-                url: "/json/users/me/api_key/regenerate",
+                // This endpoint is only accessible with the previous API key,
+                // via our usual HTTP Basic auth mechanism.
+                url: "/api/v1/users/me/api_key/regenerate",
+                headers: {Authorization: authorization_header},
                 success(data) {
                     $("#api_key_value").text(data.api_key);
                 },

programming_language: Rust
filename: test_settings.py
change_type: ModificationType.MODIFY
diff: @@ -495,7 +495,17 @@ class UserChangesTest(ZulipTestCase):
         for api_key in old_api_keys:
             self.assertEqual(get_user_profile_by_api_key(api_key).email, email)
 
+        # First verify this endpoint is not registered in the /json/... path
+        # to prevent access with only a session.
         result = self.client_post("/json/users/me/api_key/regenerate")
+        self.assertEqual(result.status_code, 404)
+
+        # A logged-in session doesn't allow access to an /api/v1/ endpoint
+        # of course.
+        result = self.client_post("/api/v1/users/me/api_key/regenerate")
+        self.assertEqual(result.status_code, 401)
+
+        result = self.api_post(user, "/api/v1/users/me/api_key/regenerate")
         self.assert_json_success(result)
         new_api_key = result.json()["api_key"]
         self.assertNotIn(new_api_key, old_api_keys)

programming_language: Python
filename: urls.py
change_type: ModificationType.MODIFY
diff: @@ -372,7 +372,6 @@ v1_api_and_json_patterns = [
     rest_path("user_groups/<int:user_group_id>", PATCH=edit_user_group, DELETE=delete_user_group),
     rest_path("user_groups/<int:user_group_id>/members", POST=update_user_group_backend),
     # users/me -> zerver.views.user_settings
-    rest_path("users/me/api_key/regenerate", POST=regenerate_api_key),
     rest_path("users/me/avatar", POST=set_avatar_backend, DELETE=delete_avatar_backend),
     # users/me/hotspots -> zerver.views.hotspots
     rest_path(
@@ -724,6 +723,11 @@ v1_api_mobile_patterns = [
     # This json format view used by the mobile apps accepts a username
     # password/pair and returns an API key.
     path("fetch_api_key", api_fetch_api_key),
+    # The endpoint for regenerating and obtaining a new API key
+    # should only be available by authenticating with the current
+    # API key - as we consider access to the API key sensitive
+    # and just having a logged-in session should be insufficient.
+    rest_path("users/me/api_key/regenerate", POST=regenerate_api_key),
 ]
 
 # View for uploading messages from email mirror

programming_language: Python