CVE-2025-67733 : RESP Injection

CVE-2025-67733 : RESP Injection

1. CVE Info

  • CVE
    • https://nvd.nist.gov/vuln/detail/CVE-2025-67733
  • PoC
    • https://github.com/JYlab/CVE-2025-67733
  • GitHub Security Advisory
    • https://github.com/valkey-io/valkey/security/advisories/GHSA-p876-p7q5-hv2m
  • Valkey Patch
    • Affected: 9.0.1 and below
    • Patched: 9.0.2, 8.1.6, 8.0.7, 7.2.12
  • Redis Patch
    • Affected: 8.4.1 and below
    • Patched: 8.4.2, 8.2.5, 8.0.6, 7.4.8, 7.2.13
  • CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:L/A:H (8.5/10)

2. Overview

This post explains the RESP injection attack. The analysis is based on Valkey version 8.1.4 source code.

This vulnerability can be exploited to cause socket poisoning on the Valkey server, allowing data forgery, as well as DoS attacks.

The attack method involves running the following Lua script via Valkey’s EVAL command.

Data Forgery PoC

To understand how this can trigger a vulnerability, it is necessary to understand RESP, Lua script execution flow, the redis.error_reply() server API, and the error() built-in function.

Therefore, I will first explain the background knowledge before sharing the vulnerability analysis. Following that, I will walk through the details of the vulnerability itself.


3. About RESP (Redis Serialization Protocol)

RESP (Redis Serialization Protocol) is the serialization protocol used for client↔server command/response communication in Valkey and Redis. It is delimited by a type prefix and CRLF (\r\n), and Bulk types explicitly specify their length.

The main types (Simple String, Error, Integer, Bulk String, Array) and usage examples are as follows.

Simple String (success message)

  • Protocol: +
  • Example: +OK\r\n, +PONG\r\n

Error (failure reason)

  • Protocol: -
  • Example: -ERR unknown command 'HELLO'\r\n

Integer (integer return - count/increment result, etc.)

  • Protocol: :
  • Example: :0\r\n, :123\r\n

Bulk String (length + data - value/binary)

  • Protocol: $
  • Example: $5\r\nhello\r\n, $-1\r\n (null), $0\r\n\r\n (empty)

Array (list of multiple RESP values - commands are usually in this form)

  • Protocol: *
  • Example: *0\r\n, *2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n, *3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n

4. Lua Script Execution Flow

When a Lua script is executed via the EVAL command in Valkey, it is processed in the following flow.

EVAL command → luaCallFunction() → Lua script execution → (on error) → error handling → client response

luaCallFunction()

luaCallFunction() is the core function that executes the Lua script and handles the result or error.

luaCallFunction

The key points of this function are:

  1. Executes the Lua script with lua_pcall()
  2. On error, extracts error information with luaExtractErrorInformation()
  3. Depending on the error type, sends the response via luaReplyToRedisReply() or directly

How the error message is constructed and sent to the client when an error occurs is the core of this vulnerability.


5. How the redis.error_reply() Server API Works

Valkey’s Lua script engine provides a feature to return errors as a table when an error occurs. redis.error_reply() is the API used to define such error tables in a custom way.

luaRedisErrorReplyCommand

Calling this server API invokes the C function luaRedisErrorReplyCommand(). This function checks the number of parameters and whether the value is a string. If the string does not start with -, it prepends - and passes the result to luaPushErrorBuff() as the err_buff argument. This is because the RESP Error protocol format starts with -.


6. The error() Built-in Function

error() is a Lua built-in function that immediately halts script execution and raises an error. When a Lua script in Valkey calls error(), the value passed as an argument is sent to the client as an error response.

error() example

The key point here is that the error table returned by redis.error_reply() can be passed as the argument to error(). In this case, the value of the err field in the error table is sent directly to the client as a RESP Error response.

The attack flow is as follows:

  1. redis.error_reply(...) → Creates an error table containing a malicious payload
  2. error(error table) → Halts the script and triggers the error response
  3. Server → Sends the error message to the client (the vulnerability occurs in this process)

In other words, error() acts as the trigger that actually delivers the malicious payload created by redis.error_reply() to the client.

The following figure shows a portion of the luaCallFunction() function, where the error value raised by error() is captured by lua_pcall().

error() code


7. The luaPushErrorBuff() Function

The following figure shows a portion of the luaPushErrorBuff() function. It separates the error code from the input err_buff, trims \r\n with sdstrim(), and then constructs final_msg to populate the err field of the error table.

luaPushErrorBuff

The important part here is that the vulnerability is caused by the sdstrim() function. The sdstrim(msg, "\r\n") function removes any \r or \n characters found at both ends of the msg string.

The problem with this function is that it does not remove \r\n from the entire string — it only removes \r\n from both ends of the string.


8. Vulnerability: Data Forgery via Socket Poisoning

Let us examine how a vulnerable error message is actually written to the client buffer. (Here, ‘client buffer’ refers to the server-side output buffer maintained per client connection, i.e., c->buf.)

Full Flow

luaCallFunction()
    ↓
luaExtractErrorInformation() → err_info.msg = "X\r\n+FAKE"
    ↓
sdscatfmt(final_msg, "-%s", err_info.msg) → final_msg = "-X\r\n+FAKE"
    ↓
addReplyErrorSdsEx(c, final_msg, flags)
    ↓
addReplyErrorLength(c, err, sdslen(err))
    ↓
addReplyProto(c, s, len)  ← s = "-X\r\n+FAKE"
addReplyProto(c, "\r\n", 2)
    ↓
_addReplyToBufferOrList(c, s, len)
    ↓
_addReplyToBuffer(c, s, len)
    ↓
memcpy(c->buf + c->bufpos, s, reply_len)  ← Direct write to client buffer

Detail

1. addReplyErrorSdsEx() - networking.c:706

Within this code, the addReplyErrorLength function is called, appending the error message to the buffer.

addReplyErrorSdsEx

This function passes the error message through without any sanitization such as sdsmapchars().

2. addReplyErrorLength() - networking.c:565

addReplyErrorLength

If the error message already starts with -, it is sent as-is. It calls the addReplyProto() function to place the value of s into the buffer.

3. _addReplyToBuffer() - networking.c:403

addReplyToBuffer

Finally, data is written directly to the client’s output buffer (c->buf) via memcpy().

Therefore, when an attacker executes error(redis.error_reply("X\r\n+FAKE")), the buffer is filled as follows.

Client Buffer State

Since the client recognizes \r\n as the RESP message boundary, it parses this as two independent responses:

  1. -X\r\n → First response (Error)
  2. +FAKE\r\n → Second response (Simple String) - processed as the response to the next command

Therefore, when a subsequent PING command is sent, instead of the server’s actual response (+PONG\r\n), the +FAKE remaining in the buffer is returned. This is how socket poisoning becomes possible.


9. Vulnerability: DoS

By leveraging RESP injection, it is possible to put the client into a waiting state, enabling a denial-of-service attack.

The RESP Bulk String type uses the format $length\r\ndata\r\n, and the client waits until it has received the number of bytes specified by the length.

If an attacker injects an enormous length value,

DoS Attack Payload

the client buffer is filled as follows.

DoS Client Buffer State

The attack scenario is roughly as follows:

  1. The first response -X\r\n is processed normally (Error)
  2. The second response $999999999\r\n is parsed
  3. The client waits for 999,999,999 bytes of data
  4. Since no data arrives, the client behaves abnormally

If this attack is performed simultaneously from multiple connections, a server-level DoS can occur through the following mechanism:

  • All client connections hang
  • Server connection slots are exhausted
  • New clients cannot connect → Entire service goes down