highCVE-2026-25243, CVE-2026-25589CVSS 8.8Postgres · Redis

Redis RESTORE RCE: patch again, then take RESTORE away

Authenticated RCE chains against every supported Redis line, demoed against the May security releases everyone patched to. Upgrade to this week's releases, then take the RESTORE command away from anyone who doesn't strictly need it.

Marko ZivanovicJul 24, 202610 min read

01 The Big PictureFor leadership · no jargon

Okay, so if you run Redis, this is one of those weeks. On Thursday a research team published working exploits that turn any logged-in Redis session into code execution on the server, covering every supported version line, and the awkward detail is that two of their demo victims, 7.4.9 and 8.6.4, are the security releases everyone deployed in May. Redis answered the same day with seven fresh security releases, so step one is: patch again. Yes, again. (Sorry.)

For the lucky few who've never met it: Redis is the little in-memory data store that sits behind a huge chunk of the world's web apps, holding sessions, queues and caches. Every exploit chain here walks in through one command, RESTORE, whose whole job is loading saved data back into the database. Think of it as the warehouse loading dock: crates roll in off the lorry and nobody checks what's inside, and it turns out you can post a crate through that hatch that unpacks itself into full control of the building. The saving grace is that the dock has a roller shutter. Almost no application needs RESTORE in day-to-day running, and Redis lets you bolt it shut, per user, in one command.

So the work comes in two steps. Patch to Thursday's release for your version line, then take RESTORE away from every user that doesn't strictly need it. The second step earns its keep because, per the exploit authors, one of their three chains still works on the newest release. It's minutes per instance and no data loss; the only real trade-off is that anything genuinely migrating data between instances needs its own service account. And yes, we'll get to the "it needs a login first, so who cares" debate.

02 Technical BreakdownFor engineers

All three chains share a front door: RESTORE, the command that deserialises an RDB snapshot payload into a live key, and until recently did so with remarkably few questions asked. An authenticated user (or literally anyone, on the tens of thousands of passwordless instances still sitting on the internet) can feed the server a crafted blob and reach code paths no honest client ever touches.

Chain one is the rude one, because it's a patch bypass. May's fixes for CVE-2026-25243 (CVSS 8.8 per NVD, found by Emil Lerner and Joseph Surin at the Wiz ZeroDay Cloud event) closed a pair of double-frees reachable via RESTORE, including one in stream consumer groups: the patch rejected a duplicate pending entry within a single consumer's list. The PoC walks straight around it by crafting a stream where two different consumers share one pending entry, then calling XGROUP DELCONSUMER for each, which frees the same 24-byte chunk twice. From there it's classic heap feng shui: an APPEND spray lands doctored dictionary entries on the freed chunk, fake string headers turn ordinary GETRANGE/SETRANGE into arbitrary read and write, a spot of Lua hands over the memory addresses (EVAL is used purely for leaks, no sandbox escape required), and the endgame swaps a hash-table function pointer for the address of system(). One more command lookup and you're running code as the redis user. The complete fix, rejecting a shared pending entry across consumers at load time, is redis PR #15081, which landed in 8.8.0 and in Thursday's batch for the other branches. China's CERT has given the bypass its own ID (QVD-2026-44936) while the CVE paperwork catches up.

Chains two and three live in RedisBloom, the probabilistic-structures module that Redis 8 bundles and loads by default. Chain two is a heap overflow in the TDigest loader, no CVE at the time of writing: the payload declares one capacity and delivers another, and the loader obligingly scribbles past the end of its allocation into a victim structure the attacker parked next door, after which ordinary TDigest commands double as a read/write oracle. Chain three is another patch bypass, this time of CVE-2026-25589 (the RedisBloom half of May's batch, fixed in RedisBloom 2.8.20): the fix's error path zeroes only some of the pointers it allocated, but the cleanup routine reads all of them, so a carefully sized failure makes the destructor free a string that's still in use. Dangling pointer, reoccupy it, arbitrary read/write. Per the PoC README that third chain still lands on Redis 8.8.1 with RedisBloom 8.8.2; I haven't seen Redis confirm or deny, afaik, so treat the newest release as "nearly there" rather than "done".

All three chains need an authenticated session, and the Hacker News thread splits predictably between "post-auth RCE is no boundary at all" and "enjoy your weekend, sysadmins". Both camps have a point, but the weekend camp wins: application credentials leak out of CI configs and staging boxes all the time, plenty of estates share one instance across several apps, and "attacker with a low-privilege app login" becoming "code execution as a server user" is a genuine boundary crossed, even before you count the passwordless instances. Also worth knowing: the exploits restore the pointers they borrow, so the server keeps running afterwards, and only the 6.2 demo wants DEBUG (enabled by default in that image). Oh, and per the team's disclosure the bugs themselves were found by AI agents doing the hunting. Make of that what you will.

03 Affected

Every supported Redis line before Thursday's releases. By branch:

| Your branch | Fixed in | Closes | |---|---|---| | 6.2.x | 6.2.23 | stream chain | | 7.2.x | 7.2.15 | stream chain | | 7.4.x | 7.4.10 | stream chain | | 8.2.x | 8.2.8 | stream + module chains | | 8.4.x | 8.4.5 | stream + module chains | | 8.6.x | 8.6.5 | stream + module chains | | 8.8.x | 8.8.1 | module chains (the stream guard was already in 8.8.0) |

The trap: 6.2.22, 7.4.9 and 8.6.4 were May's security releases, so "we patched in May" is precisely the vulnerable state. Check exact versions, not vibes. And keep the TopK caveat in mind for 8.8.1: per the PoC authors that chain still works there, which is why the ACL step below is not optional.

Exploitation needs an authenticated session plus permission to run RESTORE; the stream chain also wants EVAL and XGROUP, and the module chains want RedisBloom loaded, which Redis 8.x does by default. Anything older than 6.2 is long past end of life: treat it as vulnerable to this and plenty besides. If you run a fork such as Valkey, track the fork's own advisories rather than assuming immunity or coverage; the shared code history cuts both ways, and Red Hat has already shipped Valkey builds addressing the May CVE.

04 Detection

Before we change anything, let's see what we're dealing with. Version first, because it decides everything else:

redis-cli INFO server | grep redis_version

Anything below your branch's number in the table above is exposed. Remember: 7.4.9 and 8.6.4 are victims here, not fixes.

redis-cli ACL GETUSER default

"+@all" on the default user means every application credential is also a RESTORE credential. Repeat for each user from ACL LIST.

redis-cli MODULE LIST

On Redis 8.x, a "bf" entry is RedisBloom, loaded by default. Loaded and unused is pure attack surface.

ss -lnt | grep 6379

0.0.0.0 or a public interface means the internet can knock; you want 127.0.0.1 or a private interface.

redis-cli INFO commandstats | grep -iE 'restore|xgroup'

RESTORE calls on a system that never migrates data, or a burst of XGROUP DELCONSUMER, deserve a look. The PoC tidies up after itself, so a quiet log proves nothing, but still skim it for RDB load rejections and crash loops.

05 Remediation

01 Upgrade to Thursday's release

Pick your branch's build from the table above; packages and Docker tags carry the fixes.

docker pull redis:8.8.1

Or your branch's patched tag. With replication in place this is pleasantly boring: failover, upgrade, fail back.

Worth repeating: even 8.8.1 does not (per the PoC authors) close the TopK chain, so do not stop here.

02 Take RESTORE away from anyone who doesn't need it

redis-cli ACL SETUSER default -restore -eval -evalsha -script -xgroup -debug
redis-cli ACL SAVE

Removing RESTORE alone shuts every disclosed chain at the front door; the rest strip the helpers the exploits use for address leaks and heap grooming. This is the official workaround named in both advisories. Repeat for each application user, and hand RESTORE back only to accounts that genuinely migrate data. ACL SAVE wants an aclfile configured (check CONFIG GET aclfile); otherwise persist the change through your config management.

03 Defang the leftovers in redis.conf

rename-command RESTORE ""
rename-command EVAL ""
rename-command EVALSHA ""
rename-command SCRIPT ""
rename-command XGROUP ""
rename-command DEBUG ""

Belt and braces for instances where ACLs aren't enforced. Check the app first: renaming EVAL breaks anything using Lua, and XGROUP kills consumer groups. On 6.2, also set enable-debug-command no.

04 Drop RedisBloom if nothing uses it

redis-cli MODULE UNLOAD bf

Redis 8 loads RedisBloom by default, which is what puts the TDigest and TopK chains in scope. If the app doesn't use Bloom filters, TopK or TDigest, unload it and remove the loadmodule line from redis.conf. It will refuse while live keys of its types exist (I believe), so clear those out first.

05 Fix the exposure (the bit the comment sections are right about)

bind 127.0.0.1 -::1
requirepass <long, unique, from a vault>

The chains are post-auth, so real auth matters: no password, or "redis" as the password, turns this into a pre-auth RCE. antirez said it back in 2015 and it still holds: Redis is designed to be totally insecure if exposed, and Wiz counted roughly 330k internet-facing instances last October, about 60k with no auth at all. Two classic ways an "internal" Redis ends up public: Docker's -p 6379:6379 publishes on 0.0.0.0 and its iptables meddling bypasses ufw, so write -p 127.0.0.1:6379:6379; and SSH -L forwards bind 0.0.0.0 unless told otherwise.

06 Shrink the blast radius

Run redis-server as its dedicated low-privilege user (the distro packages already do), with a read-only root filesystem and dropped capabilities in containers, and no SYS_PTRACE anywhere in the mix. If the worst happens, you'd much rather hand an attacker "unprivileged user on a box that only runs Redis" than anything juicier.

06 Verification

Patched and locked down? Let's prove it rather than assume it.

redis-cli INFO server | grep redis_version

Your branch's number from the table, not May's.

redis-cli --user appuser --pass "$APPPW" RESTORE probe 0 "x"
(error) NOPERM this user has no permissions to run the 'restore' command

The error is the proof; run it as each application user, not just as admin. Where you renamed commands instead, expect "unknown command" rather than NOPERM.

redis-cli MODULE LIST

Empty, or at least no bf, if you dropped RedisBloom.

ss -lnt | grep 6379

Localhost or private only. Re-check after any compose or config edit; Docker will cheerfully re-publish the port.

07 Rollback

The realistic rollback is the upgrade misbehaving (a module edge case, a replication quirk), not the ACL work. Roll back to your previous pinned build while keeping the RESTORE lockdown in place; that's what makes rollback tolerable, because a May build with RESTORE bolted shut is a much smaller target than a May build wide open.

docker pull redis:8.6.4

Re-pin your previous tag or package build; keep the ACLs.

If a data-migration job genuinely needs RESTORE, scope it:

redis-cli ACL SETUSER migrator +restore

A dedicated account, enabled for the migration window, then back out.

Rolling everything back to "May patch plus default ACLs" reopens a public, working RCE with a README, so if the new release genuinely breaks something, segment the instance off and fix forward rather than unpatching.

08 References

If I've got anything wrong here, or you've hit an edge case I haven't covered, do say: corrections and questions are always welcome :)

// Patched, or stuck?

Ship the fix. Prove it’s closed.

If this runbook doesn’t fit your stack, send the CVE — an operator writes a verified path, usually within a business day. Or contribute your own and get co-authored credit.

// Dispatch

New guides, straight to your inbox.