Skip to content

Commit 30e9109

Browse files
ranshiduriyagezuiderkwast
authored
Add output buffer limiting for unauthenticated clients (#2006)
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> Signed-off-by: Ran Shidlansik <ranshid@amazon.com> Co-authored-by: uriyage <78144248+uriyage@users.noreply.github.com> Co-authored-by: Viktor Söderqvist <viktor.soderqvist@est.tech>
1 parent 83d2979 commit 30e9109

File tree

8 files changed

+95
-16
lines changed

8 files changed

+95
-16
lines changed

src/acl.c

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -495,8 +495,7 @@ void ACLFreeUserAndKillClients(user *u) {
495495
* this may result in some security hole: it's much
496496
* more defensive to set the default user and put
497497
* it in non authenticated mode. */
498-
c->user = DefaultUser;
499-
c->flag.authenticated = 0;
498+
clientSetUser(c, DefaultUser, 0);
500499
/* We will write replies to this client later, so we can't
501500
* close it directly even if async. */
502501
if (c == server.current_client) {
@@ -1455,8 +1454,8 @@ void addAuthErrReply(client *c, robj *err) {
14551454
* The return value is AUTH_OK on success (valid username / password pair) & AUTH_ERR otherwise. */
14561455
static int checkPasswordBasedAuth(client *c, robj *username, robj *password) {
14571456
if (ACLCheckUserCredentials(username, password) == C_OK) {
1458-
c->flag.authenticated = 1;
1459-
c->user = ACLGetUserByName(username->ptr, sdslen(username->ptr));
1457+
user *user = ACLGetUserByName(username->ptr, sdslen(username->ptr));
1458+
clientSetUser(c, user, 1);
14601459
moduleNotifyUserChanged(c);
14611460
return AUTH_OK;
14621461
} else {

src/debug.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,9 @@ void debugCommand(client *c) {
508508
" Grace period in seconds for replica main channel to establish psync.",
509509
"DICT-RESIZING <0|1>",
510510
" Enable or disable the main dict and expire dict resizing.",
511+
"CLIENT-ENFORCE-REPLY-LIST <0|1>",
512+
"When set to 1, it enforces the use of the client reply list directly",
513+
" and avoids using the client's static buffer.",
511514
NULL};
512515
addExtendedReplyHelp(c, help, clusterDebugCommandExtendedHelp());
513516
} else if (!strcasecmp(c->argv[1]->ptr, "segfault")) {
@@ -1020,6 +1023,9 @@ void debugCommand(client *c) {
10201023
} else if (!strcasecmp(c->argv[1]->ptr, "dict-resizing") && c->argc == 3) {
10211024
server.dict_resizing = atoi(c->argv[2]->ptr);
10221025
addReply(c, shared.ok);
1026+
} else if (!strcasecmp(c->argv[1]->ptr, "client-enforce-reply-list") && c->argc == 3) {
1027+
server.debug_client_enforce_reply_list = atoi(c->argv[2]->ptr);
1028+
addReply(c, shared.ok);
10231029
} else if (!handleDebugClusterCommand(c)) {
10241030
addReplySubcommandSyntaxError(c);
10251031
return;

src/module.c

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9703,8 +9703,7 @@ void revokeClientAuthentication(client *c) {
97039703
* is eventually freed we don't rely on the module to still exist. */
97049704
moduleNotifyUserChanged(c);
97059705

9706-
c->user = DefaultUser;
9707-
c->flag.authenticated = 0;
9706+
clientSetUser(c, DefaultUser, 0);
97089707
/* We will write replies to this client later, so we can't close it
97099708
* directly even if async. */
97109709
if (c == server.current_client) {
@@ -10032,8 +10031,7 @@ static int authenticateClientWithUser(ValkeyModuleCtx *ctx,
1003210031

1003310032
moduleNotifyUserChanged(ctx->client);
1003410033

10035-
ctx->client->user = user;
10036-
ctx->client->flag.authenticated = 1;
10034+
clientSetUser(ctx->client, user, 1);
1003710035

1003810036
if (clientHasModuleAuthInProgress(ctx->client)) {
1003910037
ctx->client->flag.module_auth_has_result = 1;

src/networking.c

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,22 @@ void linkClient(client *c) {
156156
static void clientSetDefaultAuth(client *c) {
157157
/* If the default user does not require authentication, the user is
158158
* directly authenticated. */
159-
c->user = DefaultUser;
160-
c->flag.authenticated = (c->user->flags & USER_FLAG_NOPASS) && !(c->user->flags & USER_FLAG_DISABLED);
159+
clientSetUser(c, DefaultUser, (DefaultUser->flags & USER_FLAG_NOPASS) && !(DefaultUser->flags & USER_FLAG_DISABLED));
160+
}
161+
162+
/* Attach the user u to this client.
163+
* Also, mark the client authentication state. In case the client is marked as authenticated,
164+
* it will also set the ever_authenticated flag on the client in order to avoid low level
165+
* limiting of the client output buffer.*/
166+
void clientSetUser(client *c, user *u, int authenticated) {
167+
c->user = u;
168+
c->flag.authenticated = authenticated;
169+
if (authenticated)
170+
c->flag.ever_authenticated = authenticated;
171+
}
172+
173+
static int clientEverAuthenticated(client *c) {
174+
return c->flag.ever_authenticated;
161175
}
162176

163177
int authRequired(client *c) {
@@ -412,7 +426,8 @@ void deleteCachedResponseClient(client *recording_client) {
412426
VALKEY_NO_SANITIZE("bounds")
413427
size_t _addReplyToBuffer(client *c, const char *s, size_t len) {
414428
size_t available = c->buf_usable_size - c->bufpos;
415-
429+
/* If the debug enforcing to use the reply list is enabled.*/
430+
if (server.debug_client_enforce_reply_list) return 0;
416431
/* If there already are entries in the reply list, we cannot
417432
* add anything more to the static buffer. */
418433
if (listLength(c->reply) > 0) return 0;
@@ -5003,6 +5018,11 @@ int checkClientOutputBufferLimits(client *c) {
50035018
int soft = 0, hard = 0, class;
50045019
unsigned long used_mem = getClientOutputBufferMemoryUsage(c);
50055020

5021+
/* For unauthenticated clients which were also never authenticated before the output buffer is limited to prevent
5022+
* them from abusing it by not reading the replies */
5023+
if (used_mem > REPLY_BUFFER_SIZE_UNAUTHENTICATED_CLIENT && authRequired(c) && !clientEverAuthenticated(c))
5024+
return 1;
5025+
50065026
class = getClientType(c);
50075027
/* For the purpose of output buffer limiting, primaries are handled
50085028
* like normal clients. */

src/replication.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2014,7 +2014,7 @@ void replicationCreatePrimaryClientWithHandler(connection *conn, int dbid, Conne
20142014
* execution is done. This is the reason why we allow blocking the replication
20152015
* connection. */
20162016
server.primary->flag.primary = 1;
2017-
server.primary->flag.authenticated = 1;
2017+
clientSetUser(server.primary, NULL, 1);
20182018

20192019
/* Allocate a private query buffer for the primary client instead of using the shared query buffer.
20202020
* This is done because the primary's query buffer data needs to be preserved for my sub-replicas to use. */
@@ -4389,7 +4389,7 @@ void establishPrimaryConnection(void) {
43894389
connSetPrivateData(server.primary->conn, server.primary);
43904390
server.primary->flag.close_after_reply = 0;
43914391
server.primary->flag.close_asap = 0;
4392-
server.primary->flag.authenticated = 1;
4392+
clientSetUser(server.primary, NULL, 1);
43934393
server.primary->last_interaction = server.unixtime;
43944394
server.repl_state = REPL_STATE_CONNECTED;
43954395
server.repl_down_since = 0;

src/server.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2786,6 +2786,7 @@ void initServer(void) {
27862786
server.reply_buffer_peak_reset_time = REPLY_BUFFER_DEFAULT_PEAK_RESET_TIME;
27872787
server.reply_buffer_resizing_enabled = 1;
27882788
server.client_mem_usage_buckets = NULL;
2789+
server.debug_client_enforce_reply_list = 0;
27892790
resetReplicationBuffer();
27902791

27912792
/* Make sure the locale is set on startup based on the config file. */

src/server.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@ struct hdr_histogram;
199199
#define PROTO_REPLY_MIN_BYTES (1024) /* the lower limit on reply buffer size */
200200
#define REDIS_AUTOSYNC_BYTES (1024 * 1024 * 4) /* Sync file every 4MB. */
201201

202-
#define REPLY_BUFFER_DEFAULT_PEAK_RESET_TIME 5000 /* 5 seconds */
202+
#define REPLY_BUFFER_DEFAULT_PEAK_RESET_TIME 5000 /* 5 seconds */
203+
#define REPLY_BUFFER_SIZE_UNAUTHENTICATED_CLIENT 1024 /* 1024 bytes */
203204

204205
/* When configuring the server eventloop, we setup it so that the total number
205206
* of file descriptors we can handle are server.maxclients + RESERVED_FDS +
@@ -1088,6 +1089,7 @@ typedef struct ClientFlags {
10881089
uint64_t reprocessing_command : 1; /* The client is re-processing the command. */
10891090
uint64_t replication_done : 1; /* Indicate that replication has been done on the client */
10901091
uint64_t authenticated : 1; /* Indicate a client has successfully authenticated */
1092+
uint64_t ever_authenticated : 1; /* Indicate a client was ever successfully authenticated during it's lifetime */
10911093
uint64_t protected_rdb_channel : 1; /* Dual channel replication sync: Protects the RDB client from premature \
10921094
* release during full sync. This flag is used to ensure that the RDB client, which \
10931095
* references the first replication data block required by the replica, is not \
@@ -1112,7 +1114,7 @@ typedef struct ClientFlags {
11121114
or client::buf. */
11131115
uint64_t keyspace_notified : 1; /* Indicates that a keyspace notification was triggered during the execution of the
11141116
current command. */
1115-
uint64_t reserved : 2; /* Reserved for future use */
1117+
uint64_t reserved : 1; /* Reserved for future use */
11161118
} ClientFlags;
11171119

11181120
typedef struct ClientPubSubData {
@@ -1672,7 +1674,7 @@ struct valkeyServer {
16721674
int enable_debug_cmd; /* Enable DEBUG commands, see PROTECTED_ACTION_ALLOWED_* */
16731675
int enable_module_cmd; /* Enable MODULE commands, see PROTECTED_ACTION_ALLOWED_* */
16741676
int enable_debug_assert; /* Enable debug asserts */
1675-
1677+
int debug_client_enforce_reply_list; /* Force client to always use the reply list */
16761678
/* RDB / AOF loading information */
16771679
volatile sig_atomic_t loading; /* We are loading data from disk if true */
16781680
volatile sig_atomic_t async_loading; /* We are loading data without blocking the db being served */
@@ -2765,6 +2767,7 @@ void initSharedQueryBuf(void);
27652767
void freeSharedQueryBuf(void);
27662768
client *lookupClientByID(uint64_t id);
27672769
int authRequired(client *c);
2770+
void clientSetUser(client *c, user *u, int authenticated);
27682771
void putClientInPendingWriteQueue(client *c);
27692772
client *createCachedResponseClient(int resp);
27702773
void deleteCachedResponseClient(client *recording_client);

tests/unit/auth.tcl

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,58 @@ start_server {tags {"auth external:skip"} overrides {requirepass foobar}} {
4545
assert_match {*unauthenticated bulk length*} $e
4646
$rr close
4747
}
48+
49+
test {For unauthenticated clients output buffer is limited} {
50+
set rr [valkey [srv "host"] [srv "port"] 1 $::tls]
51+
52+
# make sure the client is no longer authenticated
53+
$rr SET x 5
54+
catch {[$rr read]} e
55+
assert_match {*NOAUTH Authentication required*} $e
56+
57+
# Force clients to use the reply list in order
58+
# to make them have a larger than 1K output buffer on any reply
59+
assert_match "OK" [r debug client-enforce-reply-list 1]
60+
61+
# Fill the output buffer without reading it and make
62+
# sure the client disconnected.
63+
$rr SET x 5
64+
catch {[$rr read]} e
65+
assert_match {I/O error reading reply} $e
66+
67+
# Reset the debug
68+
assert_match "OK" [r debug client-enforce-reply-list 0]
69+
}
70+
71+
test {For once authenticated clients output buffer is NOT limited} {
72+
set rr [valkey [srv "host"] [srv "port"] 1 $::tls]
73+
74+
# first time authenticate client
75+
$rr auth foobar
76+
assert_equal {OK} [$rr read]
77+
78+
# now reset the client so it is not authenticated
79+
$rr reset
80+
assert_equal {RESET} [$rr read]
81+
82+
# make sure the client is no longer authenticated
83+
$rr SET x 5
84+
catch {[$rr read]} e
85+
assert_match {*NOAUTH Authentication required*} $e
86+
87+
# Force clients to use the reply list in order
88+
# to make them have a larger than 1K output buffer on any reply
89+
assert_match "OK" [r debug client-enforce-reply-list 1]
90+
91+
# Fill the output buffer without reading it and make
92+
# sure the client was NOT disconnected.
93+
$rr SET x 5
94+
catch {[$rr read]} e
95+
assert_match {*NOAUTH Authentication required*} $e
96+
97+
# Reset the debug
98+
assert_match "OK" [r debug client-enforce-reply-list 0]
99+
}
48100
}
49101

50102
foreach dualchannel {yes no} {

0 commit comments

Comments
 (0)