File strip-CRLF-from-replies.patch of Package redis.42889
From 857e9ac483b0f18b5ab60a7de2bbc248844dc772 Mon Sep 17 00:00:00 2001
From: "debing.sun" <debing.sun@redis.com>
Date: Sun, 28 Dec 2025 15:37:48 +0800
Subject: [PATCH] Strip CRLF from error and simple string replies (#826)
Because in some cases, the client put \r\n in the command parameters.
When Redis returns these parameters to the client via an error reply, the presence of \r\n in the middle can cause the client to only parse the portion before the \r\n when handling the error reply. This disrupts the protocol parsing and ultimately causes the connection to become stuck.
---
src/functions.c | 2 +-
src/module.c | 4 +---
src/networking.c | 19 +++++++++++++++++++
src/scripting.c | 7 ++-----
src/server.h | 2 ++
tests/modules/reply.c | 15 +++++++++++++++
tests/unit/functions.tcl | 9 +++++++++
tests/unit/moduleapi/reply.tcl | 16 ++++++++++++++++
tests/unit/scripting.tcl | 8 ++++++++
9 files changed, 73 insertions(+), 9 deletions(-)
Index: b/src/module.c
===================================================================
--- a/src/module.c
+++ b/src/module.c
@@ -1374,9 +1374,11 @@ int RM_ReplyWithLongLong(RedisModuleCtx
int replyWithStatus(RedisModuleCtx *ctx, const char *msg, char *prefix) {
client *c = moduleGetReplyClient(ctx);
if (c == NULL) return REDISMODULE_OK;
+ sds s = sdsmapchars(sdsnew(msg), "\r\n", " ", 2);
addReplyProto(c,prefix,strlen(prefix));
- addReplyProto(c,msg,strlen(msg));
+ addReplyProto(c,s,sdslen(s));
addReplyProto(c,"\r\n",2);
+ sdsfree(s);
return REDISMODULE_OK;
}
Index: b/src/networking.c
===================================================================
--- a/src/networking.c
+++ b/src/networking.c
@@ -494,6 +494,11 @@ void addReplyStatusFormat(client *c, con
va_start(ap,fmt);
sds s = sdscatvprintf(sdsempty(),fmt,ap);
va_end(ap);
+ /* Trim any newlines at the end (ones will be added by addReplyStatusLength) */
+ s = sdstrim(s, "\r\n");
+ /* Make sure there are no newlines in the middle of the string, otherwise
+ * invalid protocol is emitted. */
+ s = sdsmapchars(s, "\r\n", " ", 2);
addReplyStatusLength(c,s,sdslen(s));
sdsfree(s);
}
Index: b/tests/unit/scripting.tcl
===================================================================
--- a/tests/unit/scripting.tcl
+++ b/tests/unit/scripting.tcl
@@ -846,6 +846,15 @@ start_server {tags {"scripting"}} {
} e
set _ $e
} {*Script attempted to access nonexistent global variable 'print'*}
+
+ test "LUA redis.error_reply API with CRLF injection attempt" {
+ catch {
+ r eval {return redis.error_reply("X\r\n+INJECTED")} 0
+ } err
+ # The error message should have CRLF replaced with spaces
+ assert_match {X +INJECTED*} $err
+ }
+
}
# start a new server to test the large-memory tests
Index: b/tests/modules/misc.c
===================================================================
--- a/tests/modules/misc.c
+++ b/tests/modules/misc.c
@@ -288,6 +288,19 @@ int test_log_tsctx(RedisModuleCtx *ctx,
return REDISMODULE_OK;
}
+int rw_simplestring_array(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc < 2) return RedisModule_WrongArity(ctx);
+
+ RedisModule_ReplyWithArray(ctx, argc - 1);
+ for (int i = 1; i < argc; ++i) {
+ size_t len;
+ const char *str = RedisModule_StringPtrLen(argv[i], &len);
+ RedisModule_ReplyWithSimpleString(ctx, str);
+ }
+
+ return REDISMODULE_OK;
+}
+
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
@@ -322,6 +335,8 @@ int RedisModule_OnLoad(RedisModuleCtx *c
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"test.log_tsctx", test_log_tsctx,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"rw.simplestring_array",rw_simplestring_array,"",0,0,0) != REDISMODULE_OK)
+ return REDISMODULE_ERR;
return REDISMODULE_OK;
}
Index: b/tests/unit/moduleapi/misc.tcl
===================================================================
--- a/tests/unit/moduleapi/misc.tcl
+++ b/tests/unit/moduleapi/misc.tcl
@@ -106,4 +106,20 @@ start_server {tags {"modules"}} {
r test.log_tsctx "info" "Test message"
verify_log_message 0 "*<misc> Test message*" 0
}
+
+ test "RESP: redis.call reply parsing with invalid CRLF character" {
+ # When Lua parses redis.call replies, the current implementation only
+ # searches for '\r' characters without verifying that '\n' follows. If a '\r'
+ # appears in the protocol data (not as part of the CRLF delimiter), the parser
+ # incorrectly treats it as a valid '\r\n' terminator.
+ #
+ # Example: Protocol data containing "\rx=100000000000" would be parsed as:
+ # - '\rx' is treated as line terminator (should require '\r\n')
+ # - '=100000000000' is interpreted as length specifier
+ # - Lua attempts to create a massive string → Out of Memory
+ r deferred 1
+ r eval {return redis.call('rw.simplestring_array', '\rx=100000000000', 'hello')} 0
+ assert_equal [r rawread 30] "*2\r\n+ x=100000000000\r\n+hello\r\n"
+ r deferred 0
+ }
}