CVE-2017-0881: Incorrect Authorization (CWE-863)
value: An error in the implementation of an autosubscribe feature in the check_stream_exists route of the Zulip group chat application server before 1.4.3 allowed an authenticated user to subscribe to a private stream that should have required an invitation from an existing member to join. The issue affects all previously released versions of the Zulip server.
CWE description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
CVSS v3 base score: 4.3
Published: 2017-03-28T02:59Z
Repository: https://github.com/zulip/zulip
Language: Python
Files changed: zerver/tests/test_subs.py, zerver/views/streams.py
Commit date: 2017-01-29 15:30:59 -0800
Commit message: streams: Fix autosubscribe security bug (CVE-2017-0881).

A bug in Zulip's implementation of the "stream exists" endpoint meant
that any user of a Zulip server could subscribe to an invite-only
stream without needing to be invited by using the "autosubscribe"
argument.

Thanks to Rafid Aslam for discovering this issue.

Diff: diff --git a/zerver/tests/test_subs.py b/zerver/tests/test_subs.py
index 93d13971b..edc2df082 100644
--- a/zerver/tests/test_subs.py
+++ b/zerver/tests/test_subs.py
@@ -1934,10 +1934,33 @@ class SubscriptionAPITest(ZulipTestCase):
         self.assert_json_success(result)
         json = ujson.loads(result.content)
         self.assertIn("exists", json)
         self.assertTrue(json["exists"])
 
+    def test_existing_subscriptions_autosubscription_private_stream(self):
+        # type: () -> None
+        """Call /json/subscriptions/exist on an existing private stream with
+        autosubscribe should fail.
+        """
+        stream_name = "Saxony"
+        result = self.common_subscribe_to_streams("cordelia@zulip.com", [stream_name],
+                                                  invite_only=True)
+        stream = get_stream(stream_name, self.realm)
+
+        result = self.client_post("/json/subscriptions/exists",
+                                  {"stream": stream_name, "autosubscribe": True})
+        self.assert_json_success(result)
+        json = ujson.loads(result.content)
+        self.assertIn("exists", json)
+        self.assertTrue(json["exists"])
+        self.assertIn("subscribed", json)
+        # Importantly, we are not now subscribed
+        self.assertFalse(json["subscribed"])
+        self.assertEqual(Subscription.objects.filter(
+            recipient__type=Recipient.STREAM,
+            recipient__type_id=stream.id).count(), 1)
+
     def get_subscription(self, user_profile, stream_name):
         # type: (UserProfile, Text) -> Subscription
         stream = get_stream(stream_name, self.realm)
         return Subscription.objects.get(
             user_profile=user_profile,
diff --git a/zerver/views/streams.py b/zerver/views/streams.py
index c1df070fb..0dada0032 100644
--- a/zerver/views/streams.py
+++ b/zerver/views/streams.py
@@ -479,11 +479,11 @@ def stream_exists_backend(request, user_profile, stream_id, autosubscribe):
     except JsonableError:
         stream = None
     result = {"exists": bool(stream)}
     if stream is not None:
         recipient = get_recipient(Recipient.STREAM, stream.id)
-        if autosubscribe:
+        if not stream.invite_only and autosubscribe:
             bulk_add_subscriptions([stream], [user_profile])
         result["subscribed"] = is_active_subscriber(
             user_profile=user_profile,
             recipient=recipient)
 

Vulnerable code:         if autosubscribe:
Fixed code:     def test_existing_subscriptions_autosubscription_private_stream(self):
        # type: () -> None
        """Call /json/subscriptions/exist on an existing private stream with
        autosubscribe should fail.
        """
        stream_name = "Saxony"
        result = self.common_subscribe_to_streams("cordelia@zulip.com", [stream_name],
                                                  invite_only=True)
        stream = get_stream(stream_name, self.realm)

        result = self.client_post("/json/subscriptions/exists",
                                  {"stream": stream_name, "autosubscribe": True})
        self.assert_json_success(result)
        json = ujson.loads(result.content)
        self.assertIn("exists", json)
        self.assertTrue(json["exists"])
        self.assertIn("subscribed", json)
        # Importantly, we are not now subscribed
        self.assertFalse(json["subscribed"])
        self.assertEqual(Subscription.objects.filter(
            recipient__type=Recipient.STREAM,
            recipient__type_id=stream.id).count(), 1)

        if not stream.invite_only and autosubscribe: