GHSA-W6F5-V2H6-G786
Vulnerability from github – Published: 2026-09-08 20:57 – Updated: 2026-09-08 20:57Summary
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 (
clusteroption, incl. client-side sharding) this is remote command injection: shard-wideFLUSHDB, targetedDEL/SET, same-slot key theft viaGET, cache poisoning, and possible node/cluster outage. - On replication connections (
replicationoption) 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 makesdeserializeCommand()throw an uncaughtUnexpectedValueException: 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:
FLUSHDBwipes an entire shard. It has no key but is routable becauseClusterStrategy::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 servingslot('key').INFO(same fake-key routing) leaks server configuration via orphaned responses;CLUSTER FLUSHSLOTScan take a node down.- Same-slot
GET/SET/DELallow 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:
EVALcannot be reconstructed (the class isEVAL_due to the PHP reserved word),EVAL_ROfails theKeystrait validation, andEVALSHArequires a pre-loaded script. This is accidental, not a designed mitigation, and does not reduce severity —FLUSHDB/DEL/SETalone 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": [
{
"package": {
"ecosystem": "Packagist",
"name": "predis/predis"
},
"ranges": [
{
"events": [
{
"introduced": "3.0.0-RC1"
},
{
"fixed": "3.3.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-84372"
],
"database_specific": {
"cwe_ids": [
"CWE-93"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-08T20:57:50Z",
"nvd_published_at": "2026-09-01T22:17:18Z",
"severity": "CRITICAL"
},
"details": "### Summary\n\nAn improper CRLF neutralization flaw in Predis\u0027 pipeline handling on\naggregate connections lets an unauthenticated attacker who can influence any\npipelined argument \u2014 a value **or** a key, e.g. a URL slug used as a cache key \u2014\nsmuggle arbitrary Redis commands into the connection.\n\n- On **cluster** connections (`cluster` option, incl. client-side sharding) this\n is remote command injection: shard-wide `FLUSHDB`, targeted `DEL`/`SET`,\n same-slot key theft via `GET`, cache poisoning, and possible node/cluster\n outage.\n- On **replication** connections (`replication` option) it is a reliable,\n repeatable denial of service (uncaught fatal error) triggered by any value\n containing `\\r\\n`.\n\n### Details\n\nWhen a pipeline is executed over an aggregate connection,\n`AbstractAggregateConnection::write()` re-parses the already-serialized pipeline\nbuffer with `explode(\"\\r\\n\")` instead of honoring RESP length prefixes:\n\n- https://github.com/predis/predis/blob/v3.2.0/src/Connection/AbstractAggregateConnection.php#L78-L94\n - splits the buffer on `\\r\\n`, ignoring `$\u003clen\u003e` bulk lengths,\n - rebuilds each chunk via `Command::deserializeCommand()`\n (https://github.com/predis/predis/blob/v3.2.0/src/Command/Command.php#L157)\n to decide routing,\n - writes each chunk to the connection chosen for that (fake) command.\n\nRESP is length-prefixed, so the Redis **server** parses the original stream\ncorrectly \u2014 but this second, client-side parser treats attacker-controlled\n`\\r\\n` sequences as command boundaries. An argument such as:\n\n PAD\\r\\n*1\\r\\n$7\\r\\nFLUSHDB\n\nis a single data value to the server, but a complete, valid `FLUSHDB` command to\nthe re-parser. The consequence depends on the connection type:\n\n- **Replication:** a pipeline forces `switchToMaster()`, so all chunks go to the\n master and the byte stream stays intact \u2014 but the misaligned chunk makes\n `deserializeCommand()` throw an uncaught `UnexpectedValueException: Invalid\n serializing format`. Any value containing `\\r\\n` (binary serializers such as\n igbinary/msgpack, or multi-line text) reliably crashes the request. This is\n the crash tracked in #1574 \u2014 an unauthenticated, repeatable DoS.\n- **Cluster:** chunks are routed to different nodes by slot, so the byte stream\n is split across sockets. The smuggled command arrives on a node whose stream\n is clean and is **executed**, though the application never sent it:\n - `FLUSHDB` wipes an entire shard. It has no key but is routable because\n `ClusterStrategy::getFakeKey()` hardcodes the fake key `\u0027key\u0027`\n (https://github.com/predis/predis/blob/v3.2.0/src/Cluster/ClusterStrategy.php#L56\n and #L243-L246), so the smuggled command always lands on the node serving\n `slot(\u0027key\u0027)`.\n - `INFO` (same fake-key routing) leaks server configuration via orphaned\n responses; `CLUSTER FLUSHSLOTS` can take a node down.\n - Same-slot `GET`/`SET`/`DEL` allow key theft (the reply is attributed to the\n application\u0027s own later command on that slot), cache poisoning and targeted\n data destruction; junk-key floods can exhaust node memory (OOM / mass\n eviction of legitimate keys).\n - Lua execution is **not** reachable: `EVAL` cannot be reconstructed (the\n class is `EVAL_` due to the PHP reserved word), `EVAL_RO` fails the `Keys`\n trait validation, and `EVALSHA` requires a pre-loaded script. This is\n accidental, not a designed mitigation, and does not reduce severity \u2014\n `FLUSHDB`/`DEL`/`SET` alone already permit full cache wipes and data\n destruction.\n\n**Affected versions.** Introduced in v3.0.0 by PR #1438 (\"Improved pipeline\nabstractions\"). Affected range: **3.0.0-RC1 through 3.2.0** (v3.0.0-alpha1 is not\naffected \u2014 the vulnerable code was added after it). v1.x and v2.x are not\naffected; their pipelines write per-command via `writeRequest()` and the\nvulnerable code path does not exist.\n\nOnly `pipeline()` reaches the vulnerable sink; `transaction()` / `MULTI` paths do\nnot.\n\n### Proof of concept\n\nTwo plain `redis:8` containers acting as two shards (PredisCluster shards\nclient-side, so Redis itself need not be in cluster mode); a PHP app on a\nvulnerable Predis checkout (e.g. v3.2.0).\n\n`docker-compose.yml`:\n\n services:\n redis1:\n image: redis:8\n ports: [\"6391:6379\"]\n redis2:\n image: redis:8\n ports: [\"6392:6379\"]\n\n`index.php` (a normal-looking app \u2014 slug from URL \u2192 cache lookup):\n\n \u003c?php\n require __DIR__ . \u0027/vendor/autoload.php\u0027;\n\n $nodes = [\u0027tcp://127.0.0.1:6391\u0027, \u0027tcp://127.0.0.1:6392\u0027];\n $client = new Predis\\Client($nodes, [\u0027cluster\u0027 =\u003e \u0027predis\u0027,\n \u0027parameters\u0027 =\u003e [\u0027read_write_timeout\u0027 =\u003e 2]]);\n\n if (isset($_GET[\u0027seed\u0027])) {\n for ($i = 1; $i \u003c= 100; $i++) { $client-\u003eset(\"user:$i\", \"data$i\"); }\n exit(\u0027seeded\u0027);\n }\n\n $slug = $_GET[\u0027slug\u0027] ?? \u0027\u0027;\n try {\n [$doc] = $client-\u003epipeline()-\u003eget(\"slug:$slug\")-\u003eexecute();\n echo $doc ?: \u0027no such slug\u0027;\n } catch (Throwable $e) {\n http_response_code(500);\n echo get_class($e);\n }\n\nRun:\n\n composer require predis/predis:3.2.0\n docker compose up -d\n php -S 127.0.0.1:8080 -t .\n curl \u0027http://127.0.0.1:8080/?seed\u0027 # 100 keys\n\nAttack (smuggled `FLUSHDB` inside the slug):\n\n curl \u0027http://127.0.0.1:8080/?slug=PAD4%0D%0A*1%0D%0A%247%0D%0AFLUSHDB\u0027\n\nThe slug\u0027s first line must hash to a different shard than the fake key `\u0027key\u0027`\n(otherwise the truncated bytes swallow the injection and the request simply\n404s). With two shards this is ~50% per attempt \u2014 retry `PAD0`, `PAD1`, \u2026 until\nthe request returns 500. More shards make the attack **easier**: the per-attempt\nhit probability is `(N-1)/N`, so on production clusters with many shards the\nfirst request succeeds with near-certainty.\n\nVerified result: `dbsize` across both shards drops 100 \u2192 62; one shard was wiped\nby a `FLUSHDB` the application never issued (it only ever ran `GET`/`SET` on\nnormal keys). The fix was confirmed A/B: the same PoC wipes a shard on the parent\nof commit `053cb4b6` and fails on `053cb4b6`.\n\n### Impact\n\nCWE-93 (Improper Neutralization of CRLF Sequences) leading to Redis command\ninjection / protocol smuggling and denial of service. Any application on\n**predis/predis 3.0.0-RC1 \u2013 3.2.0** that calls `pipeline()` on a cluster or\nreplication connection and includes attacker-influenced data (values **or**\nkeys \u2014 e.g. cache keys built from URL slugs) in the pipelined commands is\naffected. This is a common pattern for cache lookups, sessions and queued\nwrites.\n\n- **Cluster:** unauthenticated remote command injection \u2014 shard-wide cache wipe\n (`FLUSHDB`), targeted destruction (`DEL`), cache poisoning (`SET`), same-slot\n key theft (`GET`), node memory exhaustion (key flood), possible cluster outage\n (`CLUSTER FLUSHSLOTS`).\n- **Replication:** reliable unauthenticated DoS on every affected request.\n\n### Remediation\n\nUpgrade to **predis/predis 3.3.0 or later**. The fix (PR #1586, commit\n`053cb4b6`) makes pipelines on aggregate connections write each command using the\nreal `Command` object, eliminating the second, byte-splitting parser.\n\nUsers who cannot upgrade immediately should avoid calling `pipeline()` on\naggregate (cluster / replication) connections with any attacker-influenced keys\nor values; there is no reliable in-application way to neutralize the embedded\n`\\r\\n` while the second parser remains in the code path.",
"id": "GHSA-w6f5-v2h6-g786",
"modified": "2026-09-08T20:57:51Z",
"published": "2026-09-08T20:57:50Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/predis/predis/security/advisories/GHSA-w6f5-v2h6-g786"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-84372"
},
{
"type": "WEB",
"url": "https://github.com/predis/predis/issues/1574"
},
{
"type": "WEB",
"url": "https://github.com/predis/predis/pull/1586"
},
{
"type": "WEB",
"url": "https://github.com/predis/predis/commit/053cb4b6ac7fb1f469ead96a78d059bc0458e408"
},
{
"type": "PACKAGE",
"url": "https://github.com/predis/predis"
},
{
"type": "WEB",
"url": "https://github.com/predis/predis/releases/tag/v3.3.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "Predis: Redis command injection and denial of service via CRLF smuggling in pipelined commands on aggregate connections"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.