CVE-2021-43799: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG) (CWE-338)
value: Zulip is an open-source team collaboration tool. Zulip Server installs RabbitMQ for internal message passing. In versions of Zulip Server prior to 4.9, the initial installation (until first reboot, or restart of RabbitMQ) does not successfully limit the default ports which RabbitMQ opens; this includes port 25672, the RabbitMQ distribution port, which is used as a management port. RabbitMQ's default "cookie" which protects this port is generated using a weak PRNG, which limits the entropy of the password to at most 36 bits; in practicality, the seed for the randomizer is biased, resulting in approximately 20 bits of entropy. If other firewalls (at the OS or network level) do not protect port 25672, a remote attacker can brute-force the 20 bits of entropy in the "cookie" and leverage it for arbitrary execution of code as the rabbitmq user. They can also read all data which is sent through RabbitMQ, which includes all message traffic sent by users. Version 4.9 contains a patch for this vulnerability. As a workaround, ensure that firewalls prevent access to ports 5672 and 25672 from outside the Zulip server.
CWE description: The product uses a Pseudo-Random Number Generator (PRNG) in a security context, but the PRNG's algorithm is not cryptographically strong.
CVSS v3 base score: 9.8
Published: 2022-01-25T21:15Z
Repository: https://github.com/zulip/zulip
Language: Other
Files changed: docs/overview/changelog.md, scripts/lib/upgrade-zulip-stage-2, scripts/setup/configure-rabbitmq, scripts/setup/generate-rabbitmq-cookie
Commit date: 2022-01-25 02:13:53 +0000
Commit message: CVE-2021-43799: Set a secure Erlang cookie.

The RabbitMQ docs state ([1]):

    RabbitMQ nodes and CLI tools (e.g. rabbitmqctl) use a cookie to
    determine whether they are allowed to communicate with each
    other. [...] The cookie is just a string of alphanumeric
    characters up to 255 characters in size. It is usually stored in a
    local file.

...and goes on to state (emphasis ours):

    If the file does not exist, Erlang VM will try to create one with
    a randomly generated value when the RabbitMQ server starts
    up. Using such generated cookie files are **appropriate in
    development environments only.**

The auto-generated cookie does not use cryptographic sources of
randomness, and generates 20 characters of `[A-Z]`.  Because of a
semi-predictable seed, the entropy of this password is thus less than
the idealized 26^20 = 94 bits of entropy; in actuality, it is 36 bits
of entropy, or potentially as low as 20 if the performance of the
server is known.

These sizes are well within the scope of remote brute-force attacks.

On provision, install, and upgrade, replace the default insecure
20-character Erlang cookie with a cryptographically secure
255-character string (the max length allowed).

[1] https://www.rabbitmq.com/clustering.html#erlang-cookie

Diff: diff --git a/docs/overview/changelog.md b/docs/overview/changelog.md
index e9aba888350..64bf575f337 100644
--- a/docs/overview/changelog.md
+++ b/docs/overview/changelog.md
@@ -119,10 +119,11 @@ log][commit-log] for an up-to-date list of raw changes.
   using the django-stubs project to get Django stubs.
 - Added development environment support for M1 Mac hardware.
 
 ## Zulip 4.x series
 
+- CVE-2021-43799: Remote execution of code involving RabbitMQ.
 - Closed access to RabbitMQ port 25672; initial installs tried to
   close this port, but failed to restart RabbitMQ for the
   configuration.
 - Removed the `rabbitmq.nodename` configuration in `zulip.conf`; all
   RabbitMQ instances will be reconfigured to have a nodename of
diff --git a/scripts/lib/upgrade-zulip-stage-2 b/scripts/lib/upgrade-zulip-stage-2
index 23747f350f5..1bc2f45729d 100755
--- a/scripts/lib/upgrade-zulip-stage-2
+++ b/scripts/lib/upgrade-zulip-stage-2
@@ -11,11 +11,11 @@ import logging
 import os
 import re
 import subprocess
 import sys
 import time
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Optional
 
 if TYPE_CHECKING:
     from typing import NoReturn
 
 os.environ["PYTHONUNBUFFERED"] = "y"
@@ -114,19 +114,35 @@ config_file = get_config_file()
 IS_SERVER_UP = True
 
 
 # Check if rabbitmq port 25672 is listening on anything except 127.0.0.1
 rabbitmq_dist_listen = listening_publicly(25672)
+# Check the erlang magic cookie size
+cookie_size: Optional[int] = None
+if os.path.exists("/var/lib/rabbitmq/.erlang.cookie"):
+    with open("/var/lib/rabbitmq/.erlang.cookie", "r") as cookie_fh:
+        cookie_size = len(cookie_fh.readline())
+else:
+    logging.info("No RabbitMQ erlang cookie found, not auditing RabbitMQ security.")
 if args.skip_puppet and rabbitmq_dist_listen:
     logging.error(
         "RabbitMQ is publicly-accessible on %s; this is a security vulnerability!",
         ", ".join(rabbitmq_dist_listen),
     )
+    issue = "issue"
+    if cookie_size is not None and cookie_size == 20:
+        # See the below comment -- this is used as a lightweight
+        # signal for a cookie made with Erlang's bad randomizer.
+        logging.error(
+            "RabbitMQ erlang cookie is insecure; this is a critical security vulnerability!"
+        )
+        issue = "issues"
     logging.error(
-        "To fix the above security issue, re-run the upgrade without --skip-puppet "
+        "To fix the above security %s, re-run the upgrade without --skip-puppet "
         "(which may be set in /etc/zulip/zulip.conf), in order to restart the "
-        "necessary services.  Running zulip-puppet-apply by itself is not sufficient."
+        "necessary services.  Running zulip-puppet-apply by itself is not sufficient.",
+        issue,
     )
     sys.exit(1)
 
 
 def shutdown_server() -> None:
@@ -302,10 +318,20 @@ if (not args.skip_puppet or migrations_needed) and IS_SERVER_UP:
 if rabbitmq_dist_listen:
     shutdown_server()
     logging.info("Shutting down rabbitmq to adjust its ports...")
     subprocess.check_call(["/usr/sbin/service", "rabbitmq-server", "stop"])
 
+if cookie_size is not None and cookie_size == 20:
+    # Checking for a 20-character cookie is used as a signal that it
+    # was generated by Erlang's insecure randomizer, which only
+    # provides between 20 and 36 bits of entropy; were it 20
+    # characters long by a good randomizer, it would be 96 bits and
+    # more than sufficient.  We generate, using good randomness, a
+    # 255-character cookie, the max allowed length.
+    logging.info("Generating a secure erlang cookie...")
+    subprocess.check_call(["./scripts/setup/generate-rabbitmq-cookie"])
+
 # Adjust Puppet class names for the manifest renames in the 4.0 release
 class_renames = {
     "zulip::app_frontend": "zulip::profile::app_frontend",
     "zulip::dockervoyager": "zulip::profile::docker",
     "zulip::memcached": "zulip::profile::memcached",
diff --git a/scripts/setup/configure-rabbitmq b/scripts/setup/configure-rabbitmq
index 372b5cd9b30..e80b4f921b2 100755
--- a/scripts/setup/configure-rabbitmq
+++ b/scripts/setup/configure-rabbitmq
@@ -8,10 +8,14 @@ if [ "$EUID" -eq 0 ]; then
     sudo=()
 else
     sudo=(sudo)
 fi
 
+# If the RabbitMQ distribution cookie is insecure, reset it and
+# restart RabbitMQ.
+"${sudo[@]}" "$(dirname "$0")/generate-rabbitmq-cookie"
+
 RABBITMQ_USERNAME=$("$(dirname "$0")/../get-django-setting" RABBITMQ_USERNAME)
 RABBITMQ_PASSWORD=$("$(dirname "$0")/../get-django-setting" RABBITMQ_PASSWORD)
 
 # Wait for RabbitMQ to start up
 try_ping() {
diff --git a/scripts/setup/generate-rabbitmq-cookie b/scripts/setup/generate-rabbitmq-cookie
new file mode 100755
index 00000000000..c54fde643e6
--- /dev/null
+++ b/scripts/setup/generate-rabbitmq-cookie
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+#
+# rabbitmq sets an insecure "magic cookie" which is used for auth;
+# reset it to be longer.
+set -eu
+
+cookiefile=/var/lib/rabbitmq/.erlang.cookie
+# If the RabbitMQ distribution cookie is insecure, reset it
+if [ ! -f "$cookiefile" ] || ! size="$(wc -c "$cookiefile")" || [ "${size%% *}" -le 20 ]; then
+    running=0
+    if service rabbitmq-server status >/dev/null; then
+        running=1
+        service rabbitmq-server stop
+    fi
+
+    echo "Setting a more secure RabbitMQ distribution magic cookie"
+    cookie="$(LC_ALL=C tr -dc '[:alnum:]' </dev/urandom | head -c255)"
+    [ "${#cookie}" -eq 255 ] # make sure tr wasn’t OOM-killed
+    tmpfile="$(mktemp "$cookiefile.XXXXXXXXXX")"
+    chown rabbitmq: "$tmpfile"
+    printf '%s' "$cookie" >"$tmpfile"
+    mv "$tmpfile" "$cookiefile"
+
+    if [ "$running" == "1" ]; then
+        service rabbitmq-server start
+    fi
+fi

Vulnerable code: from typing import TYPE_CHECKING
        "To fix the above security issue, re-run the upgrade without --skip-puppet "
        "necessary services.  Running zulip-puppet-apply by itself is not sufficient."
Fixed code: - CVE-2021-43799: Remote execution of code involving RabbitMQ.
from typing import TYPE_CHECKING, Optional
# Check the erlang magic cookie size
cookie_size: Optional[int] = None
if os.path.exists("/var/lib/rabbitmq/.erlang.cookie"):
    with open("/var/lib/rabbitmq/.erlang.cookie", "r") as cookie_fh:
        cookie_size = len(cookie_fh.readline())
else:
    logging.info("No RabbitMQ erlang cookie found, not auditing RabbitMQ security.")
    issue = "issue"
    if cookie_size is not None and cookie_size == 20:
        # See the below comment -- this is used as a lightweight
        # signal for a cookie made with Erlang's bad randomizer.
        logging.error(
            "RabbitMQ erlang cookie is insecure; this is a critical security vulnerability!"
        )
        issue = "issues"
        "To fix the above security %s, re-run the upgrade without --skip-puppet "
        "necessary services.  Running zulip-puppet-apply by itself is not sufficient.",
        issue,
if cookie_size is not None and cookie_size == 20:
    # Checking for a 20-character cookie is used as a signal that it
    # was generated by Erlang's insecure randomizer, which only
    # provides between 20 and 36 bits of entropy; were it 20
    # characters long by a good randomizer, it would be 96 bits and
    # more than sufficient.  We generate, using good randomness, a
    # 255-character cookie, the max allowed length.
    logging.info("Generating a secure erlang cookie...")
    subprocess.check_call(["./scripts/setup/generate-rabbitmq-cookie"])

# If the RabbitMQ distribution cookie is insecure, reset it and
# restart RabbitMQ.
"${sudo[@]}" "$(dirname "$0")/generate-rabbitmq-cookie"

#!/usr/bin/env bash
#
# rabbitmq sets an insecure "magic cookie" which is used for auth;
# reset it to be longer.
set -eu

cookiefile=/var/lib/rabbitmq/.erlang.cookie
# If the RabbitMQ distribution cookie is insecure, reset it
if [ ! -f "$cookiefile" ] || ! size="$(wc -c "$cookiefile")" || [ "${size%% *}" -le 20 ]; then
    running=0
    if service rabbitmq-server status >/dev/null; then
        running=1
        service rabbitmq-server stop
    fi

    echo "Setting a more secure RabbitMQ distribution magic cookie"
    cookie="$(LC_ALL=C tr -dc '[:alnum:]' </dev/urandom | head -c255)"
    [ "${#cookie}" -eq 255 ] # make sure tr wasn’t OOM-killed
    tmpfile="$(mktemp "$cookiefile.XXXXXXXXXX")"
    chown rabbitmq: "$tmpfile"
    printf '%s' "$cookie" >"$tmpfile"
    mv "$tmpfile" "$cookiefile"

    if [ "$running" == "1" ]; then
        service rabbitmq-server start
    fi
fi