Version in base suite: 7.0.15-1~deb12u6 Base version: redis_7.0.15-1~deb12u6 Target version: redis_7.0.15-1~deb12u7 Base file: /srv/ftp-master.debian.org/ftp/pool/main/r/redis/redis_7.0.15-1~deb12u6.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/r/redis/redis_7.0.15-1~deb12u7.dsc changelog | 13 ++ patches/CVE-2025-67733.patch | 224 +++++++++++++++++++++++++++++++++++++++++++ patches/CVE-2026-21863.patch | 184 +++++++++++++++++++++++++++++++++++ patches/series | 2 4 files changed, 423 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmptm9rujj_/redis_7.0.15-1~deb12u6.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmptm9rujj_/redis_7.0.15-1~deb12u7.dsc: no acceptable signature found diff -Nru redis-7.0.15/debian/changelog redis-7.0.15/debian/changelog --- redis-7.0.15/debian/changelog 2025-10-07 15:23:11.000000000 +0000 +++ redis-7.0.15/debian/changelog 2026-05-13 04:00:00.000000000 +0000 @@ -1,3 +1,16 @@ +redis (5:7.0.15-1~deb12u7) bookworm-security; urgency=high + + * CVE-2025-67733: RESP protocol injection via Lua error_reply. A user + could manipulate data read by a connection by injecting CR/LF + sequences into a Redis error reply. + 6910256443c7 ("Strip CRLF from error and simple string replies"). + * CVE-2026-21863: Remote DoS with malformed Cluster bus message. A peer + could send a crafted PING/PONG/MEET packet whose gossip count or + ping-extension header exceeds the received packet length, causing + out-of-bounds reads and a server crash. + + -- Aron Xu Wed, 13 May 2026 12:00:00 +0800 + redis (5:7.0.15-1~deb12u6) bookworm-security; urgency=medium * CVE-2025-46817 / CVE-2025-46818 / CVE-2025-49844 / CVE-2025-46819 diff -Nru redis-7.0.15/debian/patches/CVE-2025-67733.patch redis-7.0.15/debian/patches/CVE-2025-67733.patch --- redis-7.0.15/debian/patches/CVE-2025-67733.patch 1970-01-01 00:00:00.000000000 +0000 +++ redis-7.0.15/debian/patches/CVE-2025-67733.patch 2026-05-13 04:00:00.000000000 +0000 @@ -0,0 +1,224 @@ +From 6910256443c74057e0d83e08c61ea0021774fa6f Mon Sep 17 00:00:00 2001 +From: "debing.sun" +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. + +[Backport from https://github.com/redis/redis/commit/6910256443c74057e0d83e08c61ea0021774fa6f] +[Adapted for redis-7.0.15: + - redis-7.0.15 already has addReplyErrorSdsSafe(); only the new + addReplyErrorSdsExSafe() and addReplyStatusSafe() helpers are added. + - functions.c hunk uses the pre-existing addReplyErrorSdsSafe(). + - tests/unit/functions.tcl test added inside the same start_server + block, before its closing brace. + - tests/unit/moduleapi/reply.tcl test added inside the proto loop, + before its closing brace (7.0.15 lacks the upstream "WRONGTYPE A + type error" anchor). + - tests/unit/scripting.tcl test added before "LUA redis.status_reply + API" anchor.] +--- +diff --git a/src/functions.c b/src/functions.c +--- a/src/functions.c ++++ b/src/functions.c +@@ -524,7 +524,7 @@ + library_name = c->argv[++i]->ptr; + continue; + } +- addReplyErrorSds(c, sdscatfmt(sdsempty(), "Unknown argument %s", next_arg->ptr)); ++ addReplyErrorSdsSafe(c, sdscatfmt(sdsempty(), "Unknown argument %s", next_arg->ptr)); + return; + } + size_t reply_len = 0; +diff --git a/src/module.c b/src/module.c +--- a/src/module.c ++++ b/src/module.c +@@ -2760,9 +2760,7 @@ + int RM_ReplyWithSimpleString(RedisModuleCtx *ctx, const char *msg) { + client *c = moduleGetReplyClient(ctx); + if (c == NULL) return REDISMODULE_OK; +- addReplyProto(c,"+",1); +- addReplyProto(c,msg,strlen(msg)); +- addReplyProto(c,"\r\n",2); ++ addReplyStatusSafe(c, msg); + return REDISMODULE_OK; + } + +diff --git a/src/networking.c b/src/networking.c +--- a/src/networking.c ++++ b/src/networking.c +@@ -596,6 +596,13 @@ + sdsfree(err); + } + ++/* See addReplyErrorLength for expectations from the input string. ++ * The string is safe to contain \r and \n anywhere. */ ++void addReplyErrorSdsExSafe(client *c, sds err, int flags) { ++ err = sdsmapchars(err, "\r\n", " ", 2); ++ addReplyErrorSdsEx(c, err, flags); ++} ++ + /* See addReplyErrorLength for expectations from the input string. */ + /* As a side effect the SDS string is freed. */ + void addReplyErrorSds(client *c, sds err) { +@@ -662,11 +669,22 @@ + addReplyStatusLength(c,status,strlen(status)); + } + ++void addReplyStatusSafe(client *c, const char *status) { ++ sds s = sdsmapchars(sdsnew(status), "\r\n", " ", 2); ++ addReplyStatusLength(c,s,sdslen(s)); ++ sdsfree(s); ++} ++ + void addReplyStatusFormat(client *c, const char *fmt, ...) { + va_list ap; + 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); + } +diff --git a/src/script_lua.c b/src/script_lua.c +--- a/src/script_lua.c ++++ b/src/script_lua.c +@@ -648,10 +648,7 @@ + lua_rawget(lua,-2); + t = lua_type(lua,-1); + if (t == LUA_TSTRING) { +- sds ok = sdsnew(lua_tostring(lua,-1)); +- sdsmapchars(ok,"\r\n"," ",2); +- addReplyStatusLength(c, ok, sdslen(ok)); +- sdsfree(ok); ++ addReplyStatusSafe(c, lua_tostring(lua,-1)); + lua_pop(lua,2); + return; + } +@@ -1764,7 +1761,7 @@ + err_info.source, + err_info.line); + } +- addReplyErrorSdsEx(c, final_msg, err_info.ignore_err_stats_update? ERR_REPLY_FLAG_NO_STATS_UPDATE : 0); ++ addReplyErrorSdsExSafe(c, final_msg, err_info.ignore_err_stats_update? ERR_REPLY_FLAG_NO_STATS_UPDATE : 0); + luaErrorInformationDiscard(&err_info); + } + lua_pop(lua,1); /* Consume the Lua error */ +diff --git a/src/server.h b/src/server.h +--- a/src/server.h ++++ b/src/server.h +@@ -2485,12 +2485,14 @@ + void addReplyOrErrorObject(client *c, robj *reply); + void afterErrorReply(client *c, const char *s, size_t len, int flags); + void addReplyErrorSdsEx(client *c, sds err, int flags); ++void addReplyErrorSdsExSafe(client *c, sds err, int flags); + void addReplyErrorSds(client *c, sds err); + void addReplyErrorSdsSafe(client *c, sds err); + void addReplyError(client *c, const char *err); + void addReplyErrorArity(client *c); + void addReplyErrorExpireTime(client *c); + void addReplyStatus(client *c, const char *status); ++void addReplyStatusSafe(client *c, const char *s); + void addReplyDouble(client *c, double d); + void addReplyLongLongWithPrefix(client *c, long long ll, char prefix); + void addReplyBigNum(client *c, const char* num, size_t len); +diff --git a/tests/modules/reply.c b/tests/modules/reply.c +--- a/tests/modules/reply.c ++++ b/tests/modules/reply.c +@@ -71,6 +71,19 @@ + 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 rw_map(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + if (argc != 2) return RedisModule_WrongArity(ctx); + +@@ -174,6 +187,8 @@ + return REDISMODULE_ERR; + if (RedisModule_CreateCommand(ctx,"rw.array",rw_array,"",0,0,0) != REDISMODULE_OK) + return REDISMODULE_ERR; ++ if (RedisModule_CreateCommand(ctx,"rw.simplestring_array",rw_simplestring_array,"",0,0,0) != REDISMODULE_OK) ++ return REDISMODULE_ERR; + if (RedisModule_CreateCommand(ctx,"rw.map",rw_map,"",0,0,0) != REDISMODULE_OK) + return REDISMODULE_ERR; + if (RedisModule_CreateCommand(ctx,"rw.attribute",rw_attribute,"",0,0,0) != REDISMODULE_OK) +diff --git a/tests/unit/functions.tcl b/tests/unit/functions.tcl +--- a/tests/unit/functions.tcl ++++ b/tests/unit/functions.tcl +@@ -1221,4 +1221,12 @@ + set _ $e + } {*Script attempted to access nonexistent global variable 'getmetatable'*} + ++ test {FUNCTION - test function list with CRLF injection attempt} { ++ r function flush ++ ++ # This test verifies that CRLF characters are properly handled ++ # to prevent protocol injection attacks when using FUNCTION LIST. ++ # The error message should have CRLF replaced with spaces. ++ assert_error {*Unknown argument X +INJECTED*} {r function list "X\r\n+INJECTED"} ++ } + } +diff --git a/tests/unit/moduleapi/reply.tcl b/tests/unit/moduleapi/reply.tcl +--- a/tests/unit/moduleapi/reply.tcl ++++ b/tests/unit/moduleapi/reply.tcl +@@ -93,6 +93,22 @@ + catch {r rw.error} e + assert_match "An error" $e + } ++ ++ test "RESP$proto: 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 ++ } + } + + test "Unload the module - replywith" { +diff --git a/tests/unit/scripting.tcl b/tests/unit/scripting.tcl +--- a/tests/unit/scripting.tcl ++++ b/tests/unit/scripting.tcl +@@ -2175,6 +2175,14 @@ + assert_equal [errorrstat ERR r] {count=1} + } + ++ test "LUA redis.error_reply API with CRLF injection attempt" { ++ catch { ++ r eval {error(redis.error_reply("X\r\n+INJECTED"))} 0 ++ } err ++ # The error message should have CRLF replaced with spaces ++ assert_match {ERR X +INJECTED*} $err ++ } ++ + test "LUA redis.status_reply API" { + r config resetstat + r readraw 1 diff -Nru redis-7.0.15/debian/patches/CVE-2026-21863.patch redis-7.0.15/debian/patches/CVE-2026-21863.patch --- redis-7.0.15/debian/patches/CVE-2026-21863.patch 1970-01-01 00:00:00.000000000 +0000 +++ redis-7.0.15/debian/patches/CVE-2026-21863.patch 2026-05-13 04:00:00.000000000 +0000 @@ -0,0 +1,184 @@ +From 416939303d2550aefff73ac180f41b84c12ba6c0 Mon Sep 17 00:00:00 2001 +From: Roshan Khatri +Date: Mon, 23 Feb 2026 18:46:43 +0000 +Subject: [PATCH] Fix for [CVE-2026-21863] Remote DoS with malformed Valkey + Cluster bus message + +Signed-off-by: Roshan Khatri +Co-authored-by: Madelyn Olson + +[Backport from Valkey commit https://github.com/valkey-io/valkey/commit/416939303d2550aefff73ac180f41b84c12ba6c0] +[Adapted for redis-7.0.15: + - redis-7.0.15 has src/cluster.c (not src/cluster_legacy.c). + - The enclosing function is clusterProcessPacket() (not + clusterIsValidPacket()). + - Drop/keep-link return value is 1 (not 0) on redis-7.0.15. + - extlen is uint16_t (not uint32_t) on redis-7.0.15; only the two + new bounds-check blocks from upstream are inserted; existing + declarations are unchanged. + - tests/unit/cluster/packet.tcl is included verbatim; the file is + not auto-registered in tests/test_helper.tcl since the upstream + Valkey patch does not register it either. start_cluster, CI, R + helpers used by the test all exist in redis-7.0.15.] +--- +diff --git a/src/cluster.c b/src/cluster.c +--- a/src/cluster.c ++++ b/src/cluster.c +@@ -2158,17 +2158,38 @@ + explen = sizeof(clusterMsg)-sizeof(union clusterMsgData); + explen += (sizeof(clusterMsgDataGossip)*count); + ++ /* Make sure that the number of gossip messages fit in the remaining ++ * space in the message. */ ++ if (totlen < explen) { ++ serverLog(LL_WARNING, ++ "Received invalid %s packet with gossip count %d that exceeds " ++ "total packet length (%lld)", ++ clusterGetMessageTypeString(type), count, (unsigned long long)totlen); ++ return 1; ++ } ++ + /* If there is extension data, which doesn't have a fixed length, + * loop through them and validate the length of it now. */ + if (hdr->mflags[0] & CLUSTERMSG_FLAG0_EXT_DATA) { + clusterMsgPingExt *ext = getInitialPingExt(hdr, count); + while (extensions--) { ++ /* Make sure there is at least enough memory for the extension information so ++ * we can parse it. */ ++ if ((totlen - explen) < sizeof(clusterMsgPingExt)) { ++ serverLog(LL_WARNING, ++ "Received invalid %s packet with extension data that exceeds " ++ "total packet length (%lld)", ++ clusterGetMessageTypeString(type), (unsigned long long)totlen); ++ return 1; ++ } + uint16_t extlen = getPingExtLength(ext); + if (extlen % 8 != 0) { + serverLog(LL_WARNING, "Received a %s packet without proper padding (%d bytes)", + clusterGetMessageTypeString(type), (int) extlen); + return 1; + } ++ /* Similar check to earlier, but we want to make sure the extension length is valid ++ * this time. */ + if ((totlen - explen) < extlen) { + serverLog(LL_WARNING, "Received invalid %s packet with extension data that exceeds " + "total packet length (%lld)", clusterGetMessageTypeString(type), +diff --git a/tests/unit/cluster/packet.tcl b/tests/unit/cluster/packet.tcl +new file mode 100644 +index 00000000000..1b199f0b347 100644 +--- /dev/null ++++ b/tests/unit/cluster/packet.tcl +@@ -0,0 +1,113 @@ ++# Test that cluster bus messages with certain invalid packets are rejected ++# and don't crash the system. ++proc create_cluster_meet_packet {sender_name sender_port sender_cport} { ++ # Constants ++ set CLUSTER_NAMELEN 40 ++ set CLUSTER_SLOTS 16384 ++ set NET_IP_STR_LEN 46 ++ set CLUSTERMSG_TYPE_MEET 2 ++ ++ # Build the packet ++ set packet "" ++ ++ # Signature "RCmb" (4 bytes) ++ append packet "RCmb" ++ ++ # totlen (uint32_t) - will be updated at the end ++ append packet [binary format I 0] ++ ++ # ver (uint16_t) - protocol version 1 ++ append packet [binary format S 1] ++ ++ # port (uint16_t) ++ append packet [binary format S $sender_port] ++ ++ # type (uint16_t) - MEET ++ append packet [binary format S $CLUSTERMSG_TYPE_MEET] ++ ++ # count (uint16_t) - 100 gossip messages ++ # Value intentionally set to a high value, even though no gossip ++ # messages are included. ++ append packet [binary format S 100] ++ ++ # currentEpoch (uint64_t) ++ append packet [binary format W 1] ++ ++ # configEpoch (uint64_t) ++ append packet [binary format W 1] ++ ++ # offset (uint64_t) ++ append packet [binary format W 0] ++ ++ # sender[40] - node name ++ set sender_padded [string range "${sender_name}[string repeat "\x00" $CLUSTER_NAMELEN]" 0 [expr {$CLUSTER_NAMELEN - 1}]] ++ append packet $sender_padded ++ ++ # myslots[2048] - all zeros ++ append packet [string repeat "\x00" [expr {$CLUSTER_SLOTS / 8}]] ++ ++ # replicaof[40] - all zeros ++ append packet [string repeat "\x00" $CLUSTER_NAMELEN] ++ ++ # myip[46] - all zeros ++ append packet [string repeat "\x00" $NET_IP_STR_LEN] ++ ++ # extensions (uint16_t) - Set to 2 ++ append packet [binary format S 2000000000] ++ ++ # notused1[30] - reserved ++ append packet [string repeat "\x00" 30] ++ ++ # pport (uint16_t) ++ append packet [binary format S 0] ++ ++ # cport (uint16_t) - cluster bus port ++ append packet [binary format S $sender_cport] ++ ++ # flags (uint16_t) - CLUSTER_NODE_PRIMARY ++ append packet [binary format S 1] ++ ++ # state (unsigned char) - CLUSTER_OK ++ append packet [binary format c 0] ++ ++ # mflags[3] - message flags (WITH CLUSTERMSG_FLAG0_EXT_DATA flag set) ++ # CLUSTERMSG_FLAG0_EXT_DATA = (1 << 2) = 4 ++ append packet [binary format ccc 4 0 0] ++ ++ # Update totlen ++ set totlen [string length $packet] ++ set packet [string replace $packet 4 7 [binary format I $totlen]] ++ ++ return $packet ++} ++ ++start_cluster 1 0 {tags {external:skip cluster tls:skip}} { ++ test "Packet with missing gossip messages don't cause invalid read" { ++ set base_port [srv 0 port] ++ set cluster_port [expr {$base_port + 10000}] ++ set fake_node_id "abcdef1234567890abcdef1234567890abcdef12" ++ ++ # Get initial total messages received ++ set info_before [R 0 cluster info] ++ regexp {cluster_stats_messages_received:(\d+)} $info_before -> initial_received ++ ++ # Create a packet with extensions=0 but CLUSTERMSG_FLAG0_EXT_DATA flag set ++ set packet [create_cluster_meet_packet $fake_node_id $base_port $cluster_port] ++ ++ # Verify packet length ++ set packet_len [string length $packet] ++ ++ # Send the packet after configuring the socket to accept binary data ++ set sock [socket 127.0.0.1 $cluster_port] ++ fconfigure $sock -translation binary -encoding binary -buffering none -blocking 1 ++ puts -nonewline $sock $packet ++ flush $sock ++ close $sock ++ ++ wait_for_condition 1000 10 { ++ [CI 0 cluster_stats_messages_received] == 1 ++ } else { ++ fail "Packet was never received" ++ } ++ } ++} diff -Nru redis-7.0.15/debian/patches/series redis-7.0.15/debian/patches/series --- redis-7.0.15/debian/patches/series 2025-10-07 15:22:06.000000000 +0000 +++ redis-7.0.15/debian/patches/series 2026-05-13 04:00:00.000000000 +0000 @@ -14,3 +14,5 @@ CVE-2025-46818.patch CVE-2025-49844.patch CVE-2025-46819.patch +CVE-2025-67733.patch +CVE-2026-21863.patch