File CVE-2025-21605.patch of Package valkey.39739

From 507c9eb73e27745069f5259f727fadae99ef3885 Mon Sep 17 00:00:00 2001
From: uriyage <78144248+uriyage@users.noreply.github.com>
Date: Wed, 23 Apr 2025 23:30:56 +0300
Subject: [PATCH] Add output buffer limiting for unauthenticated clients
 (#1993)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This commit introduces a mechanism to track client authentication state
with a new `ever_authenticated` flag. It refactors client authentication
handling by adding a `clientSetUser` function that properly sets both
the authenticated and `ever_authenticated` flags.

The implementation limits output buffer size for clients that have never
been authenticated.

Added tests to verify the output buffer limiting behavior for
unauthenticated clients.

---------

Signed-off-by: Uri Yagelnik <uriy@amazon.com>
Signed-off-by: Viktor Söderqvist <viktor.soderqvist@est.tech>
---
 src/acl.c           |  7 +++---
 src/debug.c         |  6 ++++++
 src/module.c        |  6 ++----
 src/networking.c    | 26 ++++++++++++++++++++---
 src/replication.c   |  4 ++--
 src/server.c        |  1 +
 src/server.h        | 17 +++++++++------
 tests/unit/auth.tcl | 52 +++++++++++++++++++++++++++++++++++++++++++++
 8 files changed, 99 insertions(+), 20 deletions(-)

diff --git a/src/acl.c b/src/acl.c
index d266137d72..14087ea2f4 100644
--- a/src/acl.c
+++ b/src/acl.c
@@ -505,8 +505,7 @@ void ACLFreeUserAndKillClients(user *u) {
              * this may result in some security hole: it's much
              * more defensive to set the default user and put
              * it in non authenticated mode. */
-            c->user = DefaultUser;
-            c->flag.authenticated = 0;
+            clientSetUser(c, DefaultUser, 0);
             /* We will write replies to this client later, so we can't
              * close it directly even if async. */
             if (c == server.current_client) {
@@ -1499,8 +1498,8 @@ void addAuthErrReply(client *c, robj *err) {
  * The return value is AUTH_OK on success (valid username / password pair) & AUTH_ERR otherwise. */
 int checkPasswordBasedAuth(client *c, robj *username, robj *password) {
     if (ACLCheckUserCredentials(username, password) == C_OK) {
-        c->flag.authenticated = 1;
-        c->user = ACLGetUserByName(username->ptr, sdslen(username->ptr));
+        user *user = ACLGetUserByName(username->ptr, sdslen(username->ptr));
+        clientSetUser(c, user, 1);
         moduleNotifyUserChanged(c);
         return AUTH_OK;
     } else {
diff --git a/src/debug.c b/src/debug.c
index 7314faa2eb..a01ccf9730 100644
--- a/src/debug.c
+++ b/src/debug.c
@@ -503,6 +503,9 @@ void debugCommand(client *c) {
             "    Grace period in seconds for replica main channel to establish psync.",
             "DICT-RESIZING <0|1>",
             "    Enable or disable the main dict and expire dict resizing.",
+            "CLIENT-ENFORCE-REPLY-LIST <0|1>",
+            "    When set to 1, it enforces the use of the client reply list directly",
+            "    and avoids using the client's static buffer.",
             NULL};
         addExtendedReplyHelp(c, help, clusterDebugCommandExtendedHelp());
     } else if (!strcasecmp(c->argv[1]->ptr, "segfault")) {
@@ -1014,6 +1017,9 @@ void debugCommand(client *c) {
     } else if (!strcasecmp(c->argv[1]->ptr, "dict-resizing") && c->argc == 3) {
         server.dict_resizing = atoi(c->argv[2]->ptr);
         addReply(c, shared.ok);
+    } else if (!strcasecmp(c->argv[1]->ptr, "client-enforce-reply-list") && c->argc == 3) {
+        server.debug_client_enforce_reply_list = atoi(c->argv[2]->ptr);
+        addReply(c, shared.ok);
     } else if (!handleDebugClusterCommand(c)) {
         addReplySubcommandSyntaxError(c);
         return;
diff --git a/src/module.c b/src/module.c
index 7080293122..93d11b52d4 100644
--- a/src/module.c
+++ b/src/module.c
@@ -9505,8 +9505,7 @@ void revokeClientAuthentication(client *c) {
      * is eventually freed we don't rely on the module to still exist. */
     moduleNotifyUserChanged(c);
 
-    c->user = DefaultUser;
-    c->flag.authenticated = 0;
+    clientSetUser(c, DefaultUser, 0);
     /* We will write replies to this client later, so we can't close it
      * directly even if async. */
     if (c == server.current_client) {
@@ -9827,8 +9826,7 @@ static int authenticateClientWithUser(ValkeyModuleCtx *ctx,
 
     moduleNotifyUserChanged(ctx->client);
 
-    ctx->client->user = user;
-    ctx->client->flag.authenticated = 1;
+    clientSetUser(ctx->client, user, 1);
 
     if (clientHasModuleAuthInProgress(ctx->client)) {
         ctx->client->flag.module_auth_has_result = 1;
diff --git a/src/networking.c b/src/networking.c
index 9f4506c5fe..ab2df89a59 100644
--- a/src/networking.c
+++ b/src/networking.c
@@ -101,8 +101,22 @@ void linkClient(client *c) {
 static void clientSetDefaultAuth(client *c) {
     /* If the default user does not require authentication, the user is
      * directly authenticated. */
-    c->user = DefaultUser;
-    c->flag.authenticated = (c->user->flags & USER_FLAG_NOPASS) && !(c->user->flags & USER_FLAG_DISABLED);
+    clientSetUser(c, DefaultUser, (DefaultUser->flags & USER_FLAG_NOPASS) && !(DefaultUser->flags & USER_FLAG_DISABLED));
+}
+
+/* Attach the user u to this client.
+ * Also, mark the client authentication state. In case the client is marked as authenticated,
+ * it will also set the ever_authenticated flag on the client in order to avoid low level
+ * limiting of the client output buffer.*/
+void clientSetUser(client *c, user *u, int authenticated) {
+    c->user = u;
+    c->flag.authenticated = authenticated;
+    if (authenticated)
+        c->flag.ever_authenticated = authenticated;
+}
+
+static int clientEverAuthenticated(client *c) {
+    return c->flag.ever_authenticated;
 }
 
 int authRequired(client *c) {
@@ -378,7 +392,8 @@ void deleteCachedResponseClient(client *recording_client) {
 VALKEY_NO_SANITIZE("bounds")
 size_t _addReplyToBuffer(client *c, const char *s, size_t len) {
     size_t available = c->buf_usable_size - c->bufpos;
-
+    /* If the debug enforcing to use the reply list is enabled.*/
+    if (server.debug_client_enforce_reply_list) return 0;
     /* If there already are entries in the reply list, we cannot
      * add anything more to the static buffer. */
     if (listLength(c->reply) > 0) return 0;
@@ -4377,6 +4392,11 @@ int checkClientOutputBufferLimits(client *c) {
     int soft = 0, hard = 0, class;
     unsigned long used_mem = getClientOutputBufferMemoryUsage(c);
 
+    /* For unauthenticated clients which were also never authenticated before the output buffer is limited to prevent
+     * them from abusing it by not reading the replies */
+    if (used_mem > REPLY_BUFFER_SIZE_UNAUTHENTICATED_CLIENT && authRequired(c) && !clientEverAuthenticated(c))
+        return 1;
+
     class = getClientType(c);
     /* For the purpose of output buffer limiting, primaries are handled
      * like normal clients. */
diff --git a/src/replication.c b/src/replication.c
index daa975134d..4f87aa7df5 100644
--- a/src/replication.c
+++ b/src/replication.c
@@ -1902,7 +1902,7 @@ void replicationCreatePrimaryClientWithHandler(connection *conn, int dbid, Conne
      * execution is done. This is the reason why we allow blocking the replication
      * connection. */
     server.primary->flag.primary = 1;
-    server.primary->flag.authenticated = 1;
+    clientSetUser(server.primary, NULL, 1);
 
     /* Allocate a private query buffer for the primary client instead of using the shared query buffer.
      * This is done because the primary's query buffer data needs to be preserved for my sub-replicas to use. */
@@ -4140,7 +4140,7 @@ void establishPrimaryConnection(void) {
     connSetPrivateData(server.primary->conn, server.primary);
     server.primary->flag.close_after_reply = 0;
     server.primary->flag.close_asap = 0;
-    server.primary->flag.authenticated = 1;
+    clientSetUser(server.primary, NULL, 1);
     server.primary->last_interaction = server.unixtime;
     server.repl_state = REPL_STATE_CONNECTED;
     server.repl_down_since = 0;
diff --git a/src/server.c b/src/server.c
index 4243185b47..eb9cf04e82 100644
--- a/src/server.c
+++ b/src/server.c
@@ -2621,6 +2621,7 @@ void initServer(void) {
     server.reply_buffer_peak_reset_time = REPLY_BUFFER_DEFAULT_PEAK_RESET_TIME;
     server.reply_buffer_resizing_enabled = 1;
     server.client_mem_usage_buckets = NULL;
+    server.debug_client_enforce_reply_list = 0;
     resetReplicationBuffer();
 
     /* Make sure the locale is set on startup based on the config file. */
diff --git a/src/server.h b/src/server.h
index 82cec6c7f5..f2fce0d328 100644
--- a/src/server.h
+++ b/src/server.h
@@ -193,7 +193,8 @@ struct hdr_histogram;
 #define PROTO_REPLY_MIN_BYTES (1024)           /* the lower limit on reply buffer size */
 #define REDIS_AUTOSYNC_BYTES (1024 * 1024 * 4) /* Sync file every 4MB. */
 
-#define REPLY_BUFFER_DEFAULT_PEAK_RESET_TIME 5000 /* 5 seconds */
+#define REPLY_BUFFER_DEFAULT_PEAK_RESET_TIME 5000     /* 5 seconds */
+#define REPLY_BUFFER_SIZE_UNAUTHENTICATED_CLIENT 1024 /* 1024 bytes */
 
 /* When configuring the server eventloop, we setup it so that the total number
  * of file descriptors we can handle are server.maxclients + RESERVED_FDS +
@@ -1224,6 +1225,7 @@ typedef struct ClientFlags {
     uint64_t reprocessing_command : 1;     /* The client is re-processing the command. */
     uint64_t replication_done : 1;         /* Indicate that replication has been done on the client */
     uint64_t authenticated : 1;            /* Indicate a client has successfully authenticated */
+    uint64_t ever_authenticated : 1;       /* Indicate a client was ever successfully authenticated during it's lifetime */
     uint64_t
         protected_rdb_channel : 1; /* Dual channel replication sync: Protects the RDB client from premature \
                                     * release during full sync. This flag is used to ensure that the RDB client, which \
@@ -1244,7 +1246,7 @@ typedef struct ClientFlags {
                                       * knows that it does not need the cache and required a full sync. With this
                                       * flag, we won't cache the primary in freeClient. */
     uint64_t fake : 1;               /* This is a fake client without a real connection. */
-    uint64_t reserved : 5;           /* Reserved for future use */
+    uint64_t reserved : 4;           /* Reserved for future use */
 } ClientFlags;
 
 typedef struct client {
@@ -1771,11 +1773,11 @@ struct valkeyServer {
     int events_per_io_thread;                 /* Number of events on the event loop to trigger IO threads activation. */
     int prefetch_batch_max_size;              /* Maximum number of keys to prefetch in a single batch */
     long long events_processed_while_blocked; /* processEventsWhileBlocked() */
-    int enable_protected_configs; /* Enable the modification of protected configs, see PROTECTED_ACTION_ALLOWED_* */
-    int enable_debug_cmd;         /* Enable DEBUG commands, see PROTECTED_ACTION_ALLOWED_* */
-    int enable_module_cmd;        /* Enable MODULE commands, see PROTECTED_ACTION_ALLOWED_* */
-    int enable_debug_assert;      /* Enable debug asserts */
-
+    int enable_protected_configs;             /* Enable the modification of protected configs, see PROTECTED_ACTION_ALLOWED_* */
+    int enable_debug_cmd;                     /* Enable DEBUG commands, see PROTECTED_ACTION_ALLOWED_* */
+    int enable_module_cmd;                    /* Enable MODULE commands, see PROTECTED_ACTION_ALLOWED_* */
+    int enable_debug_assert;                  /* Enable debug asserts */
+    int debug_client_enforce_reply_list;      /* Force client to always use the reply list */
     /* RDB / AOF loading information */
     volatile sig_atomic_t loading;       /* We are loading data from disk if true */
     volatile sig_atomic_t async_loading; /* We are loading data without blocking the db being served */
@@ -2890,6 +2892,7 @@ void initSharedQueryBuf(void);
 void freeSharedQueryBuf(void);
 client *lookupClientByID(uint64_t id);
 int authRequired(client *c);
+void clientSetUser(client *c, user *u, int authenticated);
 void putClientInPendingWriteQueue(client *c);
 client *createCachedResponseClient(int resp);
 void deleteCachedResponseClient(client *recording_client);
diff --git a/tests/unit/auth.tcl b/tests/unit/auth.tcl
index 5654bf7763..0616d310f8 100644
--- a/tests/unit/auth.tcl
+++ b/tests/unit/auth.tcl
@@ -45,6 +45,58 @@ start_server {tags {"auth external:skip"} overrides {requirepass foobar}} {
         assert_match {*unauthenticated bulk length*} $e
         $rr close
     }
+
+    test {For unauthenticated clients output buffer is limited} {
+        set rr [valkey [srv "host"] [srv "port"] 1 $::tls]
+        
+        # make sure the client is no longer authenticated
+        $rr SET x 5
+        catch {[$rr read]} e
+        assert_match {*NOAUTH Authentication required*} $e
+
+        # Force clients to use the reply list in order 
+        # to make them have a larger than 1K output buffer on any reply
+        assert_match "OK" [r debug client-enforce-reply-list 1]
+        
+        # Fill the output buffer without reading it and make
+        # sure the client disconnected.
+        $rr SET x 5
+        catch {[$rr read]} e
+        assert_match {I/O error reading reply} $e  
+
+        # Reset the debug
+        assert_match "OK" [r debug client-enforce-reply-list 0]
+    }  
+
+    test {For once authenticated clients output buffer is NOT limited} {
+        set rr [valkey [srv "host"] [srv "port"] 1 $::tls]
+
+        # first time authenticate client
+        $rr auth foobar
+        assert_equal {OK} [$rr read]
+
+        # now reset the client so it is not authenticated
+        $rr reset
+        assert_equal {RESET} [$rr read]
+
+        # make sure the client is no longer authenticated
+        $rr SET x 5
+        catch {[$rr read]} e
+        assert_match {*NOAUTH Authentication required*} $e
+
+        # Force clients to use the reply list in order 
+        # to make them have a larger than 1K output buffer on any reply
+        assert_match "OK" [r debug client-enforce-reply-list 1]
+        
+        # Fill the output buffer without reading it and make
+        # sure the client was NOT disconnected.
+        $rr SET x 5
+        catch {[$rr read]} e
+        assert_match {*NOAUTH Authentication required*} $e
+
+        # Reset the debug
+        assert_match "OK" [r debug client-enforce-reply-list 0]
+    }  
 }
 
 foreach dualchannel {yes no} {
openSUSE Build Service is sponsored by