Skip to content

Commit 507c9eb

Browse files
authored
Add output buffer limiting for unauthenticated clients (#1993)
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 <[email protected]> Signed-off-by: Viktor Söderqvist <[email protected]>
1 parent ddca1c4 commit 507c9eb

File tree

8 files changed

+99
-20
lines changed

8 files changed

+99
-20
lines changed

src/acl.c

+3-4
Original file line numberDiff line numberDiff line change
@@ -505,8 +505,7 @@ void ACLFreeUserAndKillClients(user *u) {
505505
* this may result in some security hole: it's much
506506
* more defensive to set the default user and put
507507
* it in non authenticated mode. */
508-
c->user = DefaultUser;
509-
c->flag.authenticated = 0;
508+
clientSetUser(c, DefaultUser, 0);
510509
/* We will write replies to this client later, so we can't
511510
* close it directly even if async. */
512511
if (c == server.current_client) {
@@ -1499,8 +1498,8 @@ void addAuthErrReply(client *c, robj *err) {
14991498
* The return value is AUTH_OK on success (valid username / password pair) & AUTH_ERR otherwise. */
15001499
int checkPasswordBasedAuth(client *c, robj *username, robj *password) {
15011500
if (ACLCheckUserCredentials(username, password) == C_OK) {
1502-
c->flag.authenticated = 1;
1503-
c->user = ACLGetUserByName(username->ptr, sdslen(username->ptr));
1501+
user *user = ACLGetUserByName(username->ptr, sdslen(username->ptr));
1502+
clientSetUser(c, user, 1);
15041503
moduleNotifyUserChanged(c);
15051504
return AUTH_OK;
15061505
} else {

src/debug.c

+6
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,9 @@ void debugCommand(client *c) {
503503
" Grace period in seconds for replica main channel to establish psync.",
504504
"DICT-RESIZING <0|1>",
505505
" Enable or disable the main dict and expire dict resizing.",
506+
"CLIENT-ENFORCE-REPLY-LIST <0|1>",
507+
" When set to 1, it enforces the use of the client reply list directly",
508+
" and avoids using the client's static buffer.",
506509
NULL};
507510
addExtendedReplyHelp(c, help, clusterDebugCommandExtendedHelp());
508511
} else if (!strcasecmp(c->argv[1]->ptr, "segfault")) {
@@ -1014,6 +1017,9 @@ void debugCommand(client *c) {
10141017
} else if (!strcasecmp(c->argv[1]->ptr, "dict-resizing") && c->argc == 3) {
10151018
server.dict_resizing = atoi(c->argv[2]->ptr);
10161019
addReply(c, shared.ok);
1020+
} else if (!strcasecmp(c->argv[1]->ptr, "client-enforce-reply-list") && c->argc == 3) {
1021+
server.debug_client_enforce_reply_list = atoi(c->argv[2]->ptr);
1022+
addReply(c, shared.ok);
10171023
} else if (!handleDebugClusterCommand(c)) {
10181024
addReplySubcommandSyntaxError(c);
10191025
return;

src/module.c

+2-4
Original file line numberDiff line numberDiff line change
@@ -9505,8 +9505,7 @@ void revokeClientAuthentication(client *c) {
95059505
* is eventually freed we don't rely on the module to still exist. */
95069506
moduleNotifyUserChanged(c);
95079507

9508-
c->user = DefaultUser;
9509-
c->flag.authenticated = 0;
9508+
clientSetUser(c, DefaultUser, 0);
95109509
/* We will write replies to this client later, so we can't close it
95119510
* directly even if async. */
95129511
if (c == server.current_client) {
@@ -9827,8 +9826,7 @@ static int authenticateClientWithUser(ValkeyModuleCtx *ctx,
98279826

98289827
moduleNotifyUserChanged(ctx->client);
98299828

9830-
ctx->client->user = user;
9831-
ctx->client->flag.authenticated = 1;
9829+
clientSetUser(ctx->client, user, 1);
98329830

98339831
if (clientHasModuleAuthInProgress(ctx->client)) {
98349832
ctx->client->flag.module_auth_has_result = 1;

src/networking.c

+23-3
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,22 @@ void linkClient(client *c) {
101101
static void clientSetDefaultAuth(client *c) {
102102
/* If the default user does not require authentication, the user is
103103
* directly authenticated. */
104-
c->user = DefaultUser;
105-
c->flag.authenticated = (c->user->flags & USER_FLAG_NOPASS) && !(c->user->flags & USER_FLAG_DISABLED);
104+
clientSetUser(c, DefaultUser, (DefaultUser->flags & USER_FLAG_NOPASS) && !(DefaultUser->flags & USER_FLAG_DISABLED));
105+
}
106+
107+
/* Attach the user u to this client.
108+
* Also, mark the client authentication state. In case the client is marked as authenticated,
109+
* it will also set the ever_authenticated flag on the client in order to avoid low level
110+
* limiting of the client output buffer.*/
111+
void clientSetUser(client *c, user *u, int authenticated) {
112+
c->user = u;
113+
c->flag.authenticated = authenticated;
114+
if (authenticated)
115+
c->flag.ever_authenticated = authenticated;
116+
}
117+
118+
static int clientEverAuthenticated(client *c) {
119+
return c->flag.ever_authenticated;
106120
}
107121

108122
int authRequired(client *c) {
@@ -378,7 +392,8 @@ void deleteCachedResponseClient(client *recording_client) {
378392
VALKEY_NO_SANITIZE("bounds")
379393
size_t _addReplyToBuffer(client *c, const char *s, size_t len) {
380394
size_t available = c->buf_usable_size - c->bufpos;
381-
395+
/* If the debug enforcing to use the reply list is enabled.*/
396+
if (server.debug_client_enforce_reply_list) return 0;
382397
/* If there already are entries in the reply list, we cannot
383398
* add anything more to the static buffer. */
384399
if (listLength(c->reply) > 0) return 0;
@@ -4377,6 +4392,11 @@ int checkClientOutputBufferLimits(client *c) {
43774392
int soft = 0, hard = 0, class;
43784393
unsigned long used_mem = getClientOutputBufferMemoryUsage(c);
43794394

4395+
/* For unauthenticated clients which were also never authenticated before the output buffer is limited to prevent
4396+
* them from abusing it by not reading the replies */
4397+
if (used_mem > REPLY_BUFFER_SIZE_UNAUTHENTICATED_CLIENT && authRequired(c) && !clientEverAuthenticated(c))
4398+
return 1;
4399+
43804400
class = getClientType(c);
43814401
/* For the purpose of output buffer limiting, primaries are handled
43824402
* like normal clients. */

src/replication.c

+2-2
Original file line numberDiff line numberDiff line change
@@ -1902,7 +1902,7 @@ void replicationCreatePrimaryClientWithHandler(connection *conn, int dbid, Conne
19021902
* execution is done. This is the reason why we allow blocking the replication
19031903
* connection. */
19041904
server.primary->flag.primary = 1;
1905-
server.primary->flag.authenticated = 1;
1905+
clientSetUser(server.primary, NULL, 1);
19061906

19071907
/* Allocate a private query buffer for the primary client instead of using the shared query buffer.
19081908
* 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) {
41404140
connSetPrivateData(server.primary->conn, server.primary);
41414141
server.primary->flag.close_after_reply = 0;
41424142
server.primary->flag.close_asap = 0;
4143-
server.primary->flag.authenticated = 1;
4143+
clientSetUser(server.primary, NULL, 1);
41444144
server.primary->last_interaction = server.unixtime;
41454145
server.repl_state = REPL_STATE_CONNECTED;
41464146
server.repl_down_since = 0;

src/server.c

+1
Original file line numberDiff line numberDiff line change
@@ -2621,6 +2621,7 @@ void initServer(void) {
26212621
server.reply_buffer_peak_reset_time = REPLY_BUFFER_DEFAULT_PEAK_RESET_TIME;
26222622
server.reply_buffer_resizing_enabled = 1;
26232623
server.client_mem_usage_buckets = NULL;
2624+
server.debug_client_enforce_reply_list = 0;
26242625
resetReplicationBuffer();
26252626

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

src/server.h

+10-7
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,8 @@ struct hdr_histogram;
193193
#define PROTO_REPLY_MIN_BYTES (1024) /* the lower limit on reply buffer size */
194194
#define REDIS_AUTOSYNC_BYTES (1024 * 1024 * 4) /* Sync file every 4MB. */
195195

196-
#define REPLY_BUFFER_DEFAULT_PEAK_RESET_TIME 5000 /* 5 seconds */
196+
#define REPLY_BUFFER_DEFAULT_PEAK_RESET_TIME 5000 /* 5 seconds */
197+
#define REPLY_BUFFER_SIZE_UNAUTHENTICATED_CLIENT 1024 /* 1024 bytes */
197198

198199
/* When configuring the server eventloop, we setup it so that the total number
199200
* of file descriptors we can handle are server.maxclients + RESERVED_FDS +
@@ -1224,6 +1225,7 @@ typedef struct ClientFlags {
12241225
uint64_t reprocessing_command : 1; /* The client is re-processing the command. */
12251226
uint64_t replication_done : 1; /* Indicate that replication has been done on the client */
12261227
uint64_t authenticated : 1; /* Indicate a client has successfully authenticated */
1228+
uint64_t ever_authenticated : 1; /* Indicate a client was ever successfully authenticated during it's lifetime */
12271229
uint64_t
12281230
protected_rdb_channel : 1; /* Dual channel replication sync: Protects the RDB client from premature \
12291231
* release during full sync. This flag is used to ensure that the RDB client, which \
@@ -1244,7 +1246,7 @@ typedef struct ClientFlags {
12441246
* knows that it does not need the cache and required a full sync. With this
12451247
* flag, we won't cache the primary in freeClient. */
12461248
uint64_t fake : 1; /* This is a fake client without a real connection. */
1247-
uint64_t reserved : 5; /* Reserved for future use */
1249+
uint64_t reserved : 4; /* Reserved for future use */
12481250
} ClientFlags;
12491251

12501252
typedef struct client {
@@ -1771,11 +1773,11 @@ struct valkeyServer {
17711773
int events_per_io_thread; /* Number of events on the event loop to trigger IO threads activation. */
17721774
int prefetch_batch_max_size; /* Maximum number of keys to prefetch in a single batch */
17731775
long long events_processed_while_blocked; /* processEventsWhileBlocked() */
1774-
int enable_protected_configs; /* Enable the modification of protected configs, see PROTECTED_ACTION_ALLOWED_* */
1775-
int enable_debug_cmd; /* Enable DEBUG commands, see PROTECTED_ACTION_ALLOWED_* */
1776-
int enable_module_cmd; /* Enable MODULE commands, see PROTECTED_ACTION_ALLOWED_* */
1777-
int enable_debug_assert; /* Enable debug asserts */
1778-
1776+
int enable_protected_configs; /* Enable the modification of protected configs, see PROTECTED_ACTION_ALLOWED_* */
1777+
int enable_debug_cmd; /* Enable DEBUG commands, see PROTECTED_ACTION_ALLOWED_* */
1778+
int enable_module_cmd; /* Enable MODULE commands, see PROTECTED_ACTION_ALLOWED_* */
1779+
int enable_debug_assert; /* Enable debug asserts */
1780+
int debug_client_enforce_reply_list; /* Force client to always use the reply list */
17791781
/* RDB / AOF loading information */
17801782
volatile sig_atomic_t loading; /* We are loading data from disk if true */
17811783
volatile sig_atomic_t async_loading; /* We are loading data without blocking the db being served */
@@ -2890,6 +2892,7 @@ void initSharedQueryBuf(void);
28902892
void freeSharedQueryBuf(void);
28912893
client *lookupClientByID(uint64_t id);
28922894
int authRequired(client *c);
2895+
void clientSetUser(client *c, user *u, int authenticated);
28932896
void putClientInPendingWriteQueue(client *c);
28942897
client *createCachedResponseClient(int resp);
28952898
void deleteCachedResponseClient(client *recording_client);

tests/unit/auth.tcl

+52
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)