ZeroDay.cloud
Back to Blog
Redis

CVE-2026-23479: Redis Use-After-Free in unblockClientOnKey Leading to RCE

Team Xint CodeJun 212 min read

Xint Code discovered a use-after-free in Redis's blocking-client code path that leads to remote code execution.

CVE-2026-23479 - Redis Use-After-Free in unblockClientOnKey Leading to RCE


CVE-2026-23479 is a use-after-free inside Redis's blocking-client code path that allows an authenticated user to execute arbitrary operating system commands on the Redis host. The use-after-free occurs in unblockClientOnKey() (src/blocked.c), where the function calls processCommandAndResetClient() without checking whether the client was freed as a side effect before continuing to access the client structure. The vulnerability was discovered by Xint Code, a fully autonomous AI-powered security analysis tool, and a working RCE exploit was demonstrated at ZeroDay.Cloud 2025 (London, Dec 10-11, 2025). The Redis team shipped patches on May 5, 2026 across the 7.2.x, 7.4.x, 8.2.x, 8.4.x, and 8.6.x release series.

Now that patches are available, this post walks through the root cause, details the full exploit chain, and offers remediation guidance.

We'd like to thank the Redis security team for their acknowledgment and thorough resolution of this issue.

If you run Redis in production, take action now. Most authenticated Redis connections can trigger this use-after-free and execute arbitrary code on the host system.


Who Is Affected

Most Redis deployments running 7.2.0 or later are vulnerable, including any deployment where the default user retains its out-of-the-box permissions. The bug landed in 7.2-rc1 and reached general availability in 7.2.0. Patches shipped across all five maintained release series on May 5, 2026.

Release seriesAffected versionsFixed version
Redis 7.2.x7.2.0 – 7.2.137.2.14
Redis 7.4.x7.4.0 – 7.4.87.4.9
Redis 8.2.x8.2.0 – 8.2.58.2.6
Redis 8.4.x8.4.0 – 8.4.28.4.3
Redis 8.6.x8.6.0 – 8.6.28.6.3

Required Permissions

Triggering the bug requires an authenticated session with a specific set of permissions: tune client memory limits via CONFIG SET maxmemory-clients, execute Lua via EVAL, issue stream commands (XREAD, XADD), and perform basic SET/GET. In ACL terms that is @admin, @scripting, @stream, and @read/@write. In a default Redis deployment all of these are available to the default user, and in practice they are often consolidated into a single application or operator role. A deployment that denies CONFIG outright forecloses this specific exploit chain, though not the underlying use-after-free.

Ecosystem Exposure

Wiz's analysis shows that 80% of cloud environments use Redis. Out of those Redis instances, almost 85% are configured without a password, which significantly increases the attack surface.

Risk Assessment

Exploitation requires an authenticated session with privileges across the CONFIG, @scripting, and @stream command categories. Such access can be achieved by compromising a vulnerable application server or obtaining credentials from configuration.

To limit reachability, deployments should follow Redis's hardening guidance: least-privilege ACLs, network isolation, and separation of administrative and application roles. Operators should evaluate severity using the CVSS Environmental metrics to reflect the controls and exposure of their own environments.


Attack Flow and Impact

An attacker needs an authenticated Redis session with CONFIG SET, EVAL, stream commands, and basic SET/GET privileges. From there, the exploit proceeds in three phases: information leak, use-after-free trigger, and code execution.

  1. Information leak: The attacker runs a one-line Lua script that prints the string representation of redis.call, leaking a heap pointer that anchors later stages.

  2. Use-after-free: The attacker grooms client memory limits, parks a memory-bloated client on a stream, then drops the limits and wakes the stream. The eviction path frees the blocked client mid-call; unblockClientOnKey() continues to operate on freed memory. A SET reclaims the freed allocation with an attacker-controlled fake client structure.

  3. Code execution: When Redis accesses the reclaimed client to run its normal memory accounting, updateClientMemoryUsage() performs an out-of-bounds decrement against the .data section using attacker-controlled fields. The exploit aims this primitive at a GOT (Global Offset Table) entry to redirect strcasecmp() to system(), giving the attacker arbitrary command execution with the privileges of the Redis daemon: full access to every key in every database, credentials in configuration files, and a network position that can reach adjacent services.

Attack flow and impact diagram


Technical Deep Dive

CVE-2026-23479 is a use-after-free in unblockClientOnKey() (src/blocked.c).

The Vulnerable Code

unblockClientOnKey() is invoked when a key event satisfies a previously blocking command. If the client has a queued command (the CLIENT_PENDING_COMMAND flag is set), the function dispatches that command via processCommandAndResetClient(), then continues to operate on the same client pointer:

static void unblockClientOnKey(client *c, robj *key) {
    ...
    if (c->flags & CLIENT_PENDING_COMMAND) {
        c->flags &= ~CLIENT_PENDING_COMMAND;
        c->flags |= CLIENT_REEXECUTING_COMMAND;
        ...
        processCommandAndResetClient(c);
        if (!(c->flags & CLIENT_BLOCKED)) {
            if (c->flags & CLIENT_MODULE) {
                moduleCallCommandUnblockedHandler(c);
            } else {
                queueClientForReprocessing(c);
            }
        }
        exitExecutionUnit();
        afterCommand(c);
        c->flags &= ~CLIENT_REEXECUTING_COMMAND;
        ...
    }
}

The header comment for processCommandAndResetClient() explicitly warns that the client may be freed as a side effect of processing the command:

/* The function returns C_ERR in case the client was freed as a side effect
 * of processing the command, otherwise C_OK is returned. */
int processCommandAndResetClient(client *c) {

The caller ignores the return value. If processCommandAndResetClient() frees c, every subsequent dereference of c (the flags reads, the queueClientForReprocessing(c) and afterCommand(c) calls, the final flags &= ~CLIENT_REEXECUTING_COMMAND write) operates on freed memory.

The bug was introduced in two different commits. PR #11012 (January 2023) refactored the unblock-on-keys path to dispatch the queued command through the normal command pipeline, introducing the unchecked call to processCommandAndResetClient(). At that point, the call was the last thing the function did with c, so discarding the documented C_ERR return was inert. Two months later, PR #11568 (March 2023) wrapped that dispatch in execution-unit and unblock-handler bookkeeping, adding the c->flags reads, the queueClientForReprocessing(c) call, and the afterCommand(c) call after the dispatch. These two changes composed together enable the use-after-free. Both shipped in 7.2-rc1 and reached general availability in 7.2.0, so the bug exists in every stable Redis release from 7.2.0 onward.

Why This Is Exploitable

Two properties make this UAF straightforward to weaponize.

First, after processCommandAndResetClient() returns, unblockClientOnKey() immediately accesses c, but the freed slot still holds the original client's stale bytes (zfree() does not zero memory), so those reads see plausible data and the function returns without crashing. One of those reads sends c through queueClientForReprocessing(), which adds the freed pointer to server.unblocked_clients for processing at the end of the same event-loop iteration. That gap gives the attacker a deterministic reclaim window: Redis is single-threaded and processes commands from a connection in order, so an attacker who pipelines SET key value immediately behind the wake-up command can reclaim the freed allocation with a new SDS (Simple Dynamic String), before Redis drains the queue. After this reclaim, every client field is attacker-controlled!

Second, the official Redis Docker image ships redis-server with only partial RELRO (Relocation Read-Only), leaving the lazy-binding portion of the GOT (.got.plt) writable at runtime. Combined with the controllable client structure, partial RELRO turns a single attacker-chosen decrement into a full code-execution primitive via GOT overwrite.

Use-after-free lifecycle and OOB decrement primitive diagram

Exploit Steps

Stage 1: Information leak. The attacker calls a one-line Lua script that converts a Redis-side callable to its string representation:

EVAL "return tostring(redis.call)" 0

The returned function: 0x... string contains a Lua heap pointer. Subtracting a fixed Lua-side offset yields a stable address inside Redis's heap. The attacker uses this address purely to populate the fake client structure with pointers to readable memory, so any dereferences during the UAF code path stay benign until the targeted primitive fires.

Stage 2: Use-after-free trigger and heap reclaim. The attacker first arms client eviction with a high cap so the soon-to-be-victim client can accumulate a large pending reply buffer without being evicted prematurely:

CONFIG SET maxmemory-clients <high>
CONFIG SET client-output-buffer-limit "normal 0 0 0"
SET <big-key> <megabytes-of-A>

On a second connection, the evictee connection, the attacker pipelines a MULTI/EXEC block that queues many GET <big-key> calls and never reads the responses, ballooning the client's pending output buffer. Once the evictee's last_memory_usage is the largest in the server, the same connection issues a blocking XREAD against an attacker-controlled stream:

MULTI
GET <big-key>
GET <big-key>
... (~150 times)
EXEC
XREAD BLOCK 0 STREAMS <stream-key> 0-0

On a third connection, the trigger connection, the attacker drops maxmemory-clients and wakes the blocked client atomically. Wrapping both commands in a transaction is load-bearing: it delays the eviction until the right moment.

MULTI
CONFIG SET maxmemory-clients 1
XADD <stream-key> * f v
EXEC

XADD marks the stream key as ready, and once EXEC finishes, Redis drains the ready list and calls unblockClientOnKey() on the evictee. The subsequent call to processCommandAndResetClient() causes evictClients() to free the bloated evictee. Control returns to unblockClientOnKey(), which continues to access the freed c. Those reads survive because zfree() does not zero memory. One of those reads calls queueClientForReprocessing(c), which appends the freed pointer to server.unblocked_clients for processing later in the same event-loop iteration.

Next, the trigger connection's pipelined SET runs and allocates an SDS into the freed slot. The value is a fully crafted fake client structure:

SET reclaim:<rand> <fake-client-bytes>

The fake structure has two jobs: look like a valid client to every check processUnblockedClients() runs on the way to updateClientMemoryUsage(), and aim that call's OOB write at the GOT. The flags word handles the routing. The attacker sets exactly one bit, CLIENT_PENDING_COMMAND, which selects the branch reaching updateClientMemoryUsage(). Beyond flags, fields whose dereference would crash are nulled, and the rest of the slot is sprayed with the leaked Lua heap pointer. The two fields that matter to the attacker are last_memory_type, which becomes the OOB index, and last_memory_usage, which becomes the decrement.

Stage 3: GOT overwrite and command execution. When Redis drains server.unblocked_clients and treats the now-reclaimed allocation as a client, it runs its routine memory-accounting update in updateClientMemoryUsage():

void updateClientMemoryUsage(client *c) {
    ...
    /* Now that we have the memory used by the client, remove the old
     * value from the old category, and add it back. */
    server.stat_clients_type_memory[c->last_memory_type] -= c->last_memory_usage;
    ...
}

server is a global in .data, and both c->last_memory_type (the index) and c->last_memory_usage (the decrement) come from the attacker-controlled fake client. The result is an arbitrary 8-byte decrement at any offset relative to the server global. With partial RELRO the GOT remains writable, so the attacker uses these values to corrupt the GOT entry for strcasecmp, pointing it at system instead.

Note: PIE and ASLR mitigations are irrelevant here: the primitive operates relative to server, and the offset from server to got.strcasecmp is a constant of the build, not the running instance.

When the next Redis command is parsed, Redis calls strcasecmp() (which now invokes system() instead), and the command string is executed as a shell command!


Remediation

Patch Now

The fix was committed upstream and shipped in Redis 7.2.14, 7.4.9, 8.2.6, 8.4.3, and 8.6.3 on May 5, 2026.

Upgrade to the patched minor release for your series. Redis minor upgrades within a stable series are designed to be compatible; staying on an older minor carries more risk than upgrading.

Cloud managed Redis services may roll out patched versions on their own schedules. Check your provider's bulletin.

If You Can't Patch Yet

Reduce exploitability and blast radius while you schedule patching:


Responsible Disclosure Timeline


How Wiz Can Help

Wiz customers can use the pre-built query and advisory in the Wiz Threat Center to assess the risk in their environment.

Wiz identifies both internal and publicly exposed Redis instances in your environment affected by CVE-2026-23479, and alerts you to instances that have been misconfigured to allow unauthenticated access or use weak or default passwords.


Final Remarks

CVE-2026-23479 was a subtle bug stemming from an unchecked eviction side-effect. The bug shipped in Redis 7.2.0 in August 2023 and remained reachable for over two years, surviving multiple rounds of security review.

The issue was ultimately uncovered and demonstrated by Xint Code, an AI security analysis tool designed for deep vulnerability discovery in large codebases.


References