mirror of
https://github.com/predis/predis.git
synced 2026-09-15 21:07:08 +00:00
Compare commits
7 Commits
next
..
vv-1727-fix
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e6ea7f6ab | |||
| 67bc88caac | |||
| 182eb7d4ad | |||
| a4720b03ee | |||
| 37490865cc | |||
| 3a5b55e46e | |||
| 401abc4315 |
+5
-2
@@ -1,11 +1,14 @@
|
||||
## Changelog
|
||||
|
||||
## Unreleased
|
||||
### Added
|
||||
### Changed
|
||||
- Changed RESP3 double parsing to return `NAN` for NaN payloads instead of `0.0`
|
||||
- Deprecated `CommandInterface::deserializeCommand()` (CVE GHSA-w6f5-v2h6-g786)
|
||||
### Fixed
|
||||
- Fixed CRLF command injection / smuggling in `AbstractAggregateConnection::write()` (CVE GHSA-w6f5-v2h6-g786, CWE-93)
|
||||
- Fixed RESP3 double parsing returning positive `INF` for `-inf` payloads (#1716)
|
||||
- Fixed `client_info` connection parameter being ignored (#1722)
|
||||
- Fixed `Stream::write()`/`read()` leaving a dead connection when a host error handler throws exception (#1725)
|
||||
- Fixed CRLF command smuggling and node misrouting in `AbstractAggregateConnection::write()` and `CommandInterface::deserializeCommand()` (CVE GHSA-w6f5-v2h6-g786)
|
||||
|
||||
## v3.6.0 (2026-08-14)
|
||||
### Added
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
|
||||
namespace Predis\Command;
|
||||
|
||||
use Predis\ClientConfiguration;
|
||||
use UnexpectedValueException;
|
||||
|
||||
/**
|
||||
* Base class for Redis commands.
|
||||
*/
|
||||
@@ -152,4 +155,83 @@ abstract class Command implements CommandInterface
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @deprecated Not binary-safe; see CommandInterface::deserializeCommand().
|
||||
* Scheduled for removal in the next major.
|
||||
*/
|
||||
public static function deserializeCommand(string $serializedCommand): CommandInterface
|
||||
{
|
||||
$items = self::parseMultibulk($serializedCommand);
|
||||
$commandId = $items[0];
|
||||
$classPath = __NAMESPACE__ . '\Redis\\';
|
||||
|
||||
// Check if given command is a module command.
|
||||
if (count($commandIdArray = explode('.', $commandId)) > 1) {
|
||||
// Fetch module configuration to resolve namespace.
|
||||
$moduleConfiguration = array_filter(
|
||||
ClientConfiguration::getModules(),
|
||||
static function ($module) use ($commandIdArray) {
|
||||
return $module['commandPrefix'] === $commandIdArray[0];
|
||||
}
|
||||
);
|
||||
|
||||
$commandClass = strtoupper($commandIdArray[0] . $commandIdArray[1]);
|
||||
$classPath .= array_shift($moduleConfiguration)['name'] . '\\' . $commandClass;
|
||||
} else {
|
||||
$classPath .= $commandIdArray[0];
|
||||
}
|
||||
|
||||
$command = new $classPath();
|
||||
$command->setArguments(array_slice($items, 1));
|
||||
|
||||
return $command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a RESP multibulk buffer into its individual bulk-string values
|
||||
* (command ID followed by its arguments), walking each string by its own
|
||||
* declared byte length instead of splitting the buffer on "\r\n" -- which
|
||||
* a bulk string's payload may legitimately contain (see GHSA-w6f5-v2h6-g786).
|
||||
*
|
||||
* @param string $buffer
|
||||
* @return string[]
|
||||
*/
|
||||
private static function parseMultibulk(string $buffer): array
|
||||
{
|
||||
if ($buffer[0] !== '*') {
|
||||
throw new UnexpectedValueException('Invalid serializing format');
|
||||
}
|
||||
|
||||
$lineEnd = strpos($buffer, "\r\n");
|
||||
|
||||
if ($lineEnd === false) {
|
||||
throw new UnexpectedValueException('Invalid serializing format');
|
||||
}
|
||||
|
||||
$count = (int) substr($buffer, 1, $lineEnd - 1);
|
||||
$offset = $lineEnd + 2;
|
||||
$items = [];
|
||||
|
||||
for ($i = 0; $i < $count; ++$i) {
|
||||
if (($buffer[$offset] ?? '') !== '$') {
|
||||
throw new UnexpectedValueException('Invalid serializing format');
|
||||
}
|
||||
|
||||
$lineEnd = strpos($buffer, "\r\n", $offset);
|
||||
|
||||
if ($lineEnd === false) {
|
||||
throw new UnexpectedValueException('Invalid serializing format');
|
||||
}
|
||||
|
||||
$bulkLen = (int) substr($buffer, $offset + 1, $lineEnd - $offset - 1);
|
||||
$dataStart = $lineEnd + 2;
|
||||
$items[] = substr($buffer, $dataStart, $bulkLen);
|
||||
$offset = $dataStart + $bulkLen + 2;
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,4 +92,18 @@ interface CommandInterface
|
||||
* @return string
|
||||
*/
|
||||
public function serializeCommand(): string;
|
||||
|
||||
/**
|
||||
* Creates command object from given serialized representation.
|
||||
*
|
||||
* @param string $serializedCommand
|
||||
* @return static
|
||||
*
|
||||
* @deprecated Not binary-safe: it re-parses on "\r\n" and ignores RESP bulk-length
|
||||
* prefixes, so any argument containing "\r\n" is corrupted, and it
|
||||
* instantiates a command class from the parsed input. Never call it on
|
||||
* untrusted or serialized data (see CVE GHSA-w6f5-v2h6-g786). Scheduled
|
||||
* for removal in the next major.
|
||||
*/
|
||||
public static function deserializeCommand(string $serializedCommand): CommandInterface;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
|
||||
namespace Predis\Command;
|
||||
|
||||
use Predis\ClientConfiguration;
|
||||
use UnexpectedValueException;
|
||||
|
||||
/**
|
||||
* Class representing a generic Redis command.
|
||||
*
|
||||
@@ -149,4 +152,83 @@ final class RawCommand implements CommandInterface
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @deprecated Not binary-safe; see CommandInterface::deserializeCommand().
|
||||
* Scheduled for removal in the next major.
|
||||
*/
|
||||
public static function deserializeCommand(string $serializedCommand): CommandInterface
|
||||
{
|
||||
$items = self::parseMultibulk($serializedCommand);
|
||||
$commandId = $items[0];
|
||||
$classPath = __NAMESPACE__ . '\Redis\\';
|
||||
|
||||
// Check if given command is a module command.
|
||||
if (count($commandIdArray = explode('.', $commandId)) > 1) {
|
||||
// Fetch module configuration to resolve namespace.
|
||||
$moduleConfiguration = array_filter(
|
||||
ClientConfiguration::getModules(),
|
||||
static function ($module) use ($commandIdArray) {
|
||||
return $module['commandPrefix'] === $commandIdArray[0];
|
||||
}
|
||||
);
|
||||
|
||||
$commandClass = strtoupper($commandIdArray[0] . $commandIdArray[1]);
|
||||
$classPath .= array_shift($moduleConfiguration)['name'] . '\\' . $commandClass;
|
||||
} else {
|
||||
$classPath .= $commandIdArray[0];
|
||||
}
|
||||
|
||||
$command = new $classPath();
|
||||
$command->setArguments(array_slice($items, 1));
|
||||
|
||||
return $command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a RESP multibulk buffer into its individual bulk-string values
|
||||
* (command ID followed by its arguments), walking each string by its own
|
||||
* declared byte length instead of splitting the buffer on "\r\n" -- which
|
||||
* a bulk string's payload may legitimately contain (see GHSA-w6f5-v2h6-g786).
|
||||
*
|
||||
* @param string $buffer
|
||||
* @return string[]
|
||||
*/
|
||||
private static function parseMultibulk(string $buffer): array
|
||||
{
|
||||
if ($buffer[0] !== '*') {
|
||||
throw new UnexpectedValueException('Invalid serializing format');
|
||||
}
|
||||
|
||||
$lineEnd = strpos($buffer, "\r\n");
|
||||
|
||||
if ($lineEnd === false) {
|
||||
throw new UnexpectedValueException('Invalid serializing format');
|
||||
}
|
||||
|
||||
$count = (int) substr($buffer, 1, $lineEnd - 1);
|
||||
$offset = $lineEnd + 2;
|
||||
$items = [];
|
||||
|
||||
for ($i = 0; $i < $count; ++$i) {
|
||||
if (($buffer[$offset] ?? '') !== '$') {
|
||||
throw new UnexpectedValueException('Invalid serializing format');
|
||||
}
|
||||
|
||||
$lineEnd = strpos($buffer, "\r\n", $offset);
|
||||
|
||||
if ($lineEnd === false) {
|
||||
throw new UnexpectedValueException('Invalid serializing format');
|
||||
}
|
||||
|
||||
$bulkLen = (int) substr($buffer, $offset + 1, $lineEnd - $offset - 1);
|
||||
$dataStart = $lineEnd + 2;
|
||||
$items[] = substr($buffer, $dataStart, $bulkLen);
|
||||
$offset = $dataStart + $bulkLen + 2;
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
use Predis\Command\Command;
|
||||
use Predis\Command\CommandInterface;
|
||||
use Predis\NotSupportedException;
|
||||
|
||||
abstract class AbstractAggregateConnection implements AggregateConnectionInterface
|
||||
{
|
||||
@@ -77,12 +77,27 @@ abstract class AbstractAggregateConnection implements AggregateConnectionInterfa
|
||||
*/
|
||||
public function write(string $buffer): void
|
||||
{
|
||||
// Refuse raw buffers: re-splitting them on "\r\n" ignored RESP length
|
||||
// prefixes and let CRLF-smuggled commands be routed to a node
|
||||
// (CVE GHSA-w6f5-v2h6-g786). Pipelines write each command individually.
|
||||
throw new NotSupportedException(
|
||||
'Aggregate connections cannot write a raw command buffer; '
|
||||
. 'route each command through writeRequest() instead.'
|
||||
);
|
||||
$offset = 0;
|
||||
$length = strlen($buffer);
|
||||
|
||||
while ($offset < $length) {
|
||||
$start = $offset;
|
||||
$lineEnd = strpos($buffer, "\r\n", $offset);
|
||||
$argsCount = (int) substr($buffer, $offset + 1, $lineEnd - $offset - 1);
|
||||
$offset = $lineEnd + 2;
|
||||
|
||||
// Advance by each bulk string's own declared byte length rather than
|
||||
// splitting on literal "\r\n", which a bulk string's value may legitimately
|
||||
// contain (see GHSA-w6f5-v2h6-g786).
|
||||
for ($i = 0; $i < $argsCount; ++$i) {
|
||||
$lineEnd = strpos($buffer, "\r\n", $offset);
|
||||
$bulkLen = (int) substr($buffer, $offset + 1, $lineEnd - $offset - 1);
|
||||
$offset = $lineEnd + 2 + $bulkLen + 2;
|
||||
}
|
||||
|
||||
$command = substr($buffer, $start, $offset - $start);
|
||||
$commandObj = Command::deserializeCommand($command);
|
||||
$this->getConnectionByCommand($commandObj)->write($command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +205,10 @@ class Stream implements StreamInterface
|
||||
throw new RuntimeException('Cannot write to a non-writable stream');
|
||||
}
|
||||
|
||||
$result = fwrite($this->stream, $string);
|
||||
// Suppressed: some error handlers (Laravel, Symfony, Laminas) convert engine
|
||||
// notices/warnings into thrown exceptions, which would otherwise bypass the
|
||||
// return-value handling below and leave a dead connection looking "connected".
|
||||
$result = @fwrite($this->stream, $string);
|
||||
|
||||
if ($result === false || $result === 0) {
|
||||
$metadata = $this->getMetadata();
|
||||
@@ -265,10 +268,11 @@ class Stream implements StreamInterface
|
||||
return '';
|
||||
}
|
||||
|
||||
// Suppressed: see the note in write() above.
|
||||
if ($length === -1) {
|
||||
$string = fgets($this->stream);
|
||||
$string = @fgets($this->stream);
|
||||
} else {
|
||||
$string = fread($this->stream, $length);
|
||||
$string = @fread($this->stream, $length);
|
||||
}
|
||||
|
||||
if (false === $string) {
|
||||
|
||||
@@ -71,10 +71,6 @@ class Resp3Strategy extends Resp2Strategy
|
||||
return -INF;
|
||||
}
|
||||
|
||||
if (preg_match('/^-?nan(\(.*\))?$/i', $string) === 1) {
|
||||
return NAN;
|
||||
}
|
||||
|
||||
return (float) $string;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,23 @@
|
||||
|
||||
namespace Predis\Command;
|
||||
|
||||
use Predis\Command\Redis\BloomFilter\BFADD;
|
||||
use Predis\Command\Redis\CountMinSketch\CMSINFO;
|
||||
use Predis\Command\Redis\CuckooFilter\CFADD;
|
||||
use Predis\Command\Redis\GET;
|
||||
use Predis\Command\Redis\Json\JSONSET;
|
||||
use Predis\Command\Redis\MGET;
|
||||
use Predis\Command\Redis\Search\FTAGGREGATE;
|
||||
use Predis\Command\Redis\Search\FTEXPLAIN;
|
||||
use Predis\Command\Redis\Search\FTSEARCH;
|
||||
use Predis\Command\Redis\Search\FTSPELLCHECK;
|
||||
use Predis\Command\Redis\TDigest\TDIGESTADD;
|
||||
use Predis\Command\Redis\TimeSeries\TSGET;
|
||||
use Predis\Command\Redis\TopK\TOPKQUERY;
|
||||
use Predis\Command\Redis\ZADD;
|
||||
use PredisTestCase;
|
||||
use stdClass;
|
||||
use UnexpectedValueException;
|
||||
|
||||
class CommandTest extends PredisTestCase
|
||||
{
|
||||
@@ -179,4 +194,104 @@ class CommandTest extends PredisTestCase
|
||||
$command->serializeCommand()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider deserializeCommandProvider
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testDeserializeCommand(string $class, array $arguments): void
|
||||
{
|
||||
$command = new $class();
|
||||
$command->setArguments($arguments);
|
||||
|
||||
$deserializedCommand = Command::deserializeCommand($command->serializeCommand());
|
||||
|
||||
$this->assertInstanceOf($class, $deserializedCommand);
|
||||
$this->assertEquals($command->getArguments(), $deserializedCommand->getArguments());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @return void
|
||||
*/
|
||||
public function testDeserializeCommandThrowsException(): void
|
||||
{
|
||||
$this->expectException(UnexpectedValueException::class);
|
||||
$this->expectExceptionMessage('Invalid serializing format');
|
||||
|
||||
Command::deserializeCommand('foobar');
|
||||
}
|
||||
|
||||
public function deserializeCommandProvider(): array
|
||||
{
|
||||
return [
|
||||
'GET' => [
|
||||
GET::class,
|
||||
['arg1'],
|
||||
],
|
||||
'MGET' => [
|
||||
MGET::class,
|
||||
['arg1', 'arg2', 'arg3'],
|
||||
],
|
||||
'ZADD' => [
|
||||
ZADD::class,
|
||||
['key', 'value', 'key1', 'value1'],
|
||||
],
|
||||
'JSONSET' => [
|
||||
JSONSET::class,
|
||||
['key', '$', '{"key":"value"}'],
|
||||
],
|
||||
'BFADD' => [
|
||||
BFADD::class,
|
||||
['key', 'value'],
|
||||
],
|
||||
'CFADD' => [
|
||||
CFADD::class,
|
||||
['key', 'value'],
|
||||
],
|
||||
'CMSINFO' => [
|
||||
CMSINFO::class,
|
||||
['key', 'value'],
|
||||
],
|
||||
'FTSEARCH' => [
|
||||
FTSEARCH::class,
|
||||
['key', 'value', 'DIALECT', '2'],
|
||||
],
|
||||
'FTAGGREGATE' => [
|
||||
FTAGGREGATE::class,
|
||||
['key', 'value', 'DIALECT', '2'],
|
||||
],
|
||||
'FTSPELLCHECK' => [
|
||||
FTSPELLCHECK::class,
|
||||
['key', 'value', 'DIALECT', '2'],
|
||||
],
|
||||
'FTEXPLAIN' => [
|
||||
FTEXPLAIN::class,
|
||||
['key', 'value', 'DIALECT', '2'],
|
||||
],
|
||||
'TDIGESTADD' => [
|
||||
TDIGESTADD::class,
|
||||
['key', 'value'],
|
||||
],
|
||||
'TSGET' => [
|
||||
TSGET::class,
|
||||
['key'],
|
||||
],
|
||||
'TOPKQUERY' => [
|
||||
TOPKQUERY::class,
|
||||
['key'],
|
||||
],
|
||||
// Regression cases for GHSA-w6f5-v2h6-g786 (CWE-93): a "\r\n" embedded
|
||||
// in a bulk string's own byte payload must not be mistaken for a RESP
|
||||
// line boundary.
|
||||
'GET with CRLF embedded in the value' => [
|
||||
GET::class,
|
||||
["value\r\n*1\r\n\$4\r\nEVIL"],
|
||||
],
|
||||
'ZADD with CRLF embedded in the key' => [
|
||||
ZADD::class,
|
||||
["key\r\n*1\r\n\$4\r\nEVIL", 'value', 'key1', 'value1'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,19 @@
|
||||
|
||||
namespace Predis\Command;
|
||||
|
||||
use Predis\Command\Redis\BloomFilter\BFADD;
|
||||
use Predis\Command\Redis\CountMinSketch\CMSINFO;
|
||||
use Predis\Command\Redis\CuckooFilter\CFADD;
|
||||
use Predis\Command\Redis\GET;
|
||||
use Predis\Command\Redis\Json\JSONSET;
|
||||
use Predis\Command\Redis\MGET;
|
||||
use Predis\Command\Redis\Search\FTSEARCH;
|
||||
use Predis\Command\Redis\TDigest\TDIGESTADD;
|
||||
use Predis\Command\Redis\TimeSeries\TSGET;
|
||||
use Predis\Command\Redis\TopK\TOPKQUERY;
|
||||
use Predis\Command\Redis\ZADD;
|
||||
use PredisTestCase;
|
||||
use UnexpectedValueException;
|
||||
|
||||
class RawCommandTest extends PredisTestCase
|
||||
{
|
||||
@@ -151,4 +163,92 @@ class RawCommandTest extends PredisTestCase
|
||||
$command->serializeCommand()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider deserializeCommandProvider
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testDeserializeCommand(string $class, array $arguments): void
|
||||
{
|
||||
$command = new $class();
|
||||
$command->setArguments($arguments);
|
||||
|
||||
$deserializedCommand = RawCommand::deserializeCommand($command->serializeCommand());
|
||||
|
||||
$this->assertInstanceOf($class, $deserializedCommand);
|
||||
$this->assertEquals($command->getArguments(), $deserializedCommand->getArguments());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @return void
|
||||
*/
|
||||
public function testDeserializeCommandThrowsException(): void
|
||||
{
|
||||
$this->expectException(UnexpectedValueException::class);
|
||||
$this->expectExceptionMessage('Invalid serializing format');
|
||||
|
||||
RawCommand::deserializeCommand('foobar');
|
||||
}
|
||||
|
||||
public function deserializeCommandProvider(): array
|
||||
{
|
||||
return [
|
||||
'GET' => [
|
||||
GET::class,
|
||||
['arg1'],
|
||||
],
|
||||
'MGET' => [
|
||||
MGET::class,
|
||||
['arg1', 'arg2', 'arg3'],
|
||||
],
|
||||
'ZADD' => [
|
||||
ZADD::class,
|
||||
['key', 'value', 'key1', 'value1'],
|
||||
],
|
||||
'JSONSET' => [
|
||||
JSONSET::class,
|
||||
['key', '$', '{"key":"value"}'],
|
||||
],
|
||||
'BFADD' => [
|
||||
BFADD::class,
|
||||
['key', 'value'],
|
||||
],
|
||||
'CFADD' => [
|
||||
CFADD::class,
|
||||
['key', 'value'],
|
||||
],
|
||||
'CMSINFO' => [
|
||||
CMSINFO::class,
|
||||
['key', 'value'],
|
||||
],
|
||||
'FTSEARCH' => [
|
||||
FTSEARCH::class,
|
||||
['key', 'value'],
|
||||
],
|
||||
'TDIGESTADD' => [
|
||||
TDIGESTADD::class,
|
||||
['key', 'value'],
|
||||
],
|
||||
'TSGET' => [
|
||||
TSGET::class,
|
||||
['key'],
|
||||
],
|
||||
'TOPKQUERY' => [
|
||||
TOPKQUERY::class,
|
||||
['key'],
|
||||
],
|
||||
// Regression cases for GHSA-w6f5-v2h6-g786 (CWE-93): a "\r\n" embedded
|
||||
// in a bulk string's own byte payload must not be mistaken for a RESP
|
||||
// line boundary.
|
||||
'GET with CRLF embedded in the value' => [
|
||||
GET::class,
|
||||
["value\r\n*1\r\n\$4\r\nEVIL"],
|
||||
],
|
||||
'ZADD with CRLF embedded in the key' => [
|
||||
ZADD::class,
|
||||
["key\r\n*1\r\n\$4\r\nEVIL", 'value', 'key1', 'value1'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,11 +118,7 @@ class TDIGESTBYRANK_Test extends PredisCommandTestCase
|
||||
$actualResponse = $redis->tdigestbyrank('key', 0, 1, 2, 3, 4, 5, 6);
|
||||
|
||||
$this->assertEquals($expectedResponse, $actualResponse);
|
||||
$emptyResponse = $redis->tdigestbyrank('empty_key', 0, 1);
|
||||
$this->assertCount(2, $emptyResponse);
|
||||
foreach ($emptyResponse as $value) {
|
||||
$this->assertNan($value);
|
||||
}
|
||||
$this->assertEquals([null, null], $redis->tdigestbyrank('empty_key', 0, 1));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -118,11 +118,7 @@ class TDIGESTBYREVRANK_Test extends PredisCommandTestCase
|
||||
$actualResponse = $redis->tdigestbyrevrank('key', 0, 1, 2, 3, 4, 5, 6);
|
||||
|
||||
$this->assertEquals($expectedResponse, $actualResponse);
|
||||
$emptyResponse = $redis->tdigestbyrevrank('empty_key', 0, 1);
|
||||
$this->assertCount(2, $emptyResponse);
|
||||
foreach ($emptyResponse as $value) {
|
||||
$this->assertNan($value);
|
||||
}
|
||||
$this->assertEquals([null, null], $redis->tdigestbyrevrank('empty_key', 0, 1));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -118,11 +118,7 @@ class TDIGESTCDF_Test extends PredisCommandTestCase
|
||||
$actualResponse = $redis->tdigestcdf('key', 0, 1, 2, 3, 4);
|
||||
|
||||
$this->assertSameWithPrecision($expectedResponse, $actualResponse, 5);
|
||||
$emptyResponse = $redis->tdigestcdf('empty_key', 0, 1);
|
||||
$this->assertCount(2, $emptyResponse);
|
||||
foreach ($emptyResponse as $value) {
|
||||
$this->assertNan($value);
|
||||
}
|
||||
$this->assertSame([0.0, 0.0], $redis->tdigestcdf('empty_key', 0, 1));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -116,7 +116,7 @@ class TDIGESTMAX_Test extends PredisCommandTestCase
|
||||
$actualResponse = $redis->tdigestmax('key');
|
||||
|
||||
$this->assertEquals('5', $actualResponse);
|
||||
$this->assertNan($redis->tdigestmax('empty_key'));
|
||||
$this->assertEquals(0, $redis->tdigestmax('empty_key'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -116,7 +116,7 @@ class TDIGESTMIN_Test extends PredisCommandTestCase
|
||||
$actualResponse = $redis->tdigestmin('key');
|
||||
|
||||
$this->assertEquals('1', $actualResponse);
|
||||
$this->assertNan($redis->tdigestmin('empty_key'));
|
||||
$this->assertEquals(0, $redis->tdigestmin('empty_key'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -118,11 +118,7 @@ class TDIGESTQUANTILE_Test extends PredisCommandTestCase
|
||||
$this->assertEquals([1.0, 2.0, 3.0, 3.0, 4.0, 4.0, 4.0, 5.0, 5.0, 5.0, 5.0], $quantileResponse);
|
||||
|
||||
$redis->tdigestcreate('empty_key');
|
||||
$emptyResponse = $redis->tdigestquantile('empty_key', 0.0, 0.1);
|
||||
$this->assertCount(2, $emptyResponse);
|
||||
foreach ($emptyResponse as $value) {
|
||||
$this->assertNan($value);
|
||||
}
|
||||
$this->assertEquals([null, null], $redis->tdigestquantile('empty_key', 0.0, 0.1));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -129,11 +129,10 @@ class TDIGESTRESET_Test extends PredisCommandTestCase
|
||||
|
||||
$this->assertEquals('OK', $actualResponse);
|
||||
$this->assertSame(500, $info['Compression']);
|
||||
$resetResponse = $redis->tdigestbyrank('key', 0, 1, 2, 3, 4, 5);
|
||||
$this->assertCount(6, $resetResponse);
|
||||
foreach ($resetResponse as $value) {
|
||||
$this->assertNan($value);
|
||||
}
|
||||
$this->assertEquals(
|
||||
[null, null, null, null, null, null],
|
||||
$redis->tdigestbyrank('key', 0, 1, 2, 3, 4, 5)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Predis\Connection\Cluster;
|
||||
|
||||
use Predis\Command\CommandInterface;
|
||||
use Predis\Command\Redis\GET;
|
||||
use Predis\Command\Redis\SET;
|
||||
use Predis\Connection\Parameters;
|
||||
use PredisTestCase;
|
||||
|
||||
@@ -474,30 +475,114 @@ class PredisClusterTest extends PredisTestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression guard for CVE GHSA-w6f5-v2h6-g786 (CWE-93): an aggregate connection
|
||||
* must refuse a raw, already-serialized command buffer instead of re-splitting it
|
||||
* on "\r\n". The old parser ignored RESP bulk-length prefixes, so CRLF sequences
|
||||
* smuggled into a value or key were parsed as extra commands and routed to a node.
|
||||
*
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testWriteRejectsRawCommandBuffer(): void
|
||||
public function testWrite(): void
|
||||
{
|
||||
// A single GET whose key carries a smuggled FLUSHDB payload; the old code
|
||||
// would have re-parsed and routed the FLUSHDB, this must route nothing.
|
||||
$command = new GET();
|
||||
$command->setArguments(["slug:PAD\r\n*1\r\n\$7\r\nFLUSHDB"]);
|
||||
$command1 = new GET();
|
||||
$command1->setArguments(['arg1']);
|
||||
|
||||
$command2 = new GET();
|
||||
$command2->setArguments(['arg2']);
|
||||
|
||||
$command3 = new GET();
|
||||
$command3->setArguments(['arg3']);
|
||||
|
||||
$connection1 = $this->getMockConnection('tcp://127.0.0.1:7001');
|
||||
$connection2 = $this->getMockConnection('tcp://127.0.0.1:7002');
|
||||
$connection3 = $this->getMockConnection('tcp://127.0.0.1:7003');
|
||||
|
||||
$connection1
|
||||
->expects($this->exactly(3))
|
||||
->method('write')
|
||||
->withConsecutive(
|
||||
[$command1->serializeCommand()],
|
||||
[$command2->serializeCommand()],
|
||||
[$command3->serializeCommand()]
|
||||
);
|
||||
|
||||
$connection2
|
||||
->expects($this->never())
|
||||
->method('write');
|
||||
|
||||
$connection3
|
||||
->expects($this->never())
|
||||
->method('write');
|
||||
|
||||
$cluster = new PredisCluster(new Parameters());
|
||||
$cluster->add($connection1);
|
||||
|
||||
$this->expectException('Predis\NotSupportedException');
|
||||
$this->expectExceptionMessage('Aggregate connections cannot write a raw command buffer');
|
||||
$cluster->add($connection1);
|
||||
$cluster->add($connection2);
|
||||
$cluster->add($connection3);
|
||||
|
||||
$cluster->write($command1->serializeCommand() . $command2->serializeCommand() . $command3->serializeCommand());
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression guard for GHSA-w6f5-v2h6-g786 (CWE-93): a CRLF embedded in a
|
||||
* bulk string's own value must not be mistaken for a command boundary
|
||||
* (splitting one command into a smuggled extra command), and must not
|
||||
* corrupt the argument list used to pick the target node.
|
||||
*
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testWriteHandlesCRLFEmbeddedInBulkStringValue(): void
|
||||
{
|
||||
$command = new SET();
|
||||
$command->setArguments(['victim-key', "PAD\r\n*1\r\n\$7\r\nFLUSHDB"]);
|
||||
|
||||
$connection1 = $this->getMockConnection('tcp://127.0.0.1:7001');
|
||||
$connection2 = $this->getMockConnection('tcp://127.0.0.1:7002');
|
||||
|
||||
$cluster = new PredisCluster(new Parameters());
|
||||
$cluster->add($connection1);
|
||||
$cluster->add($connection2);
|
||||
|
||||
$expectedConnection = $cluster->getConnectionByCommand($command);
|
||||
$otherConnection = $expectedConnection === $connection1 ? $connection2 : $connection1;
|
||||
|
||||
$expectedConnection
|
||||
->expects($this->once())
|
||||
->method('write')
|
||||
->with($command->serializeCommand());
|
||||
|
||||
$otherConnection
|
||||
->expects($this->never())
|
||||
->method('write');
|
||||
|
||||
$cluster->write($command->serializeCommand());
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression guard for GHSA-w6f5-v2h6-g786 (CWE-93): a CRLF embedded in a
|
||||
* bulk string KEY must not corrupt the argument list used to pick the
|
||||
* target node, which would silently route the command to the wrong node.
|
||||
*
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testWriteHandlesCRLFEmbeddedInBulkStringKey(): void
|
||||
{
|
||||
$command = new SET();
|
||||
$command->setArguments(["victim\r\n*1\r\n\$4\r\nEVIL", 'somevalue']);
|
||||
|
||||
$connection1 = $this->getMockConnection('tcp://127.0.0.1:7001');
|
||||
$connection2 = $this->getMockConnection('tcp://127.0.0.1:7002');
|
||||
|
||||
$cluster = new PredisCluster(new Parameters());
|
||||
$cluster->add($connection1);
|
||||
$cluster->add($connection2);
|
||||
|
||||
$expectedConnection = $cluster->getConnectionByCommand($command);
|
||||
$otherConnection = $expectedConnection === $connection1 ? $connection2 : $connection1;
|
||||
|
||||
$expectedConnection
|
||||
->expects($this->once())
|
||||
->method('write')
|
||||
->with($command->serializeCommand());
|
||||
|
||||
$otherConnection
|
||||
->expects($this->never())
|
||||
->method('write');
|
||||
|
||||
$cluster->write($command->serializeCommand());
|
||||
}
|
||||
|
||||
@@ -1634,32 +1634,118 @@ class RedisClusterTest extends PredisTestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression guard for CVE GHSA-w6f5-v2h6-g786 (CWE-93): an aggregate connection
|
||||
* must refuse a raw, already-serialized command buffer instead of re-splitting it
|
||||
* on "\r\n". The old parser ignored RESP bulk-length prefixes, so CRLF sequences
|
||||
* smuggled into a value or key were parsed as extra commands and routed to a node.
|
||||
*
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testWriteRejectsRawCommandBuffer(): void
|
||||
public function testWrite(): void
|
||||
{
|
||||
// A single GET whose key carries a smuggled FLUSHDB payload; the old code
|
||||
// would have re-parsed and routed the FLUSHDB, this must route nothing.
|
||||
$command = new Command\Redis\GET();
|
||||
$command->setArguments(["slug:PAD\r\n*1\r\n\$7\r\nFLUSHDB"]);
|
||||
$command1 = new Command\Redis\GET();
|
||||
$command1->setArguments(['arg1']);
|
||||
|
||||
$command2 = new Command\Redis\GET();
|
||||
$command2->setArguments(['arg2']);
|
||||
|
||||
$command3 = new Command\Redis\GET();
|
||||
$command3->setArguments(['arg3']);
|
||||
|
||||
$factory = $this->getMockBuilder(FactoryInterface::class)->getMock();
|
||||
|
||||
$connection1 = $this->getMockConnection('tcp://127.0.0.1:7001');
|
||||
$connection2 = $this->getMockConnection('tcp://127.0.0.1:7002');
|
||||
$connection3 = $this->getMockConnection('tcp://127.0.0.1:7003');
|
||||
|
||||
$connection1
|
||||
->expects($this->never())
|
||||
->method('write');
|
||||
->expects($this->once())
|
||||
->method('write')
|
||||
->with($command3->serializeCommand());
|
||||
|
||||
$connection2
|
||||
->expects($this->once())
|
||||
->method('write')
|
||||
->with($command2->serializeCommand());
|
||||
|
||||
$connection3
|
||||
->expects($this->once())
|
||||
->method('write')
|
||||
->with($command1->serializeCommand());
|
||||
|
||||
$cluster = new RedisCluster($factory, new Parameters());
|
||||
|
||||
$cluster->add($connection1);
|
||||
$cluster->add($connection2);
|
||||
$cluster->add($connection3);
|
||||
|
||||
$cluster->write($command1->serializeCommand() . $command2->serializeCommand() . $command3->serializeCommand());
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression guard for GHSA-w6f5-v2h6-g786 (CWE-93): a CRLF embedded in a
|
||||
* bulk string's own value must not be mistaken for a command boundary
|
||||
* (splitting one command into a smuggled extra command), and must not
|
||||
* corrupt the argument list used to pick the target node.
|
||||
*
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testWriteHandlesCRLFEmbeddedInBulkStringValue(): void
|
||||
{
|
||||
$command = new Command\Redis\SET();
|
||||
$command->setArguments(['victim-key', "PAD\r\n*1\r\n\$7\r\nFLUSHDB"]);
|
||||
|
||||
$factory = $this->getMockBuilder(FactoryInterface::class)->getMock();
|
||||
|
||||
$connection1 = $this->getMockConnection('tcp://127.0.0.1:7001');
|
||||
$connection2 = $this->getMockConnection('tcp://127.0.0.1:7002');
|
||||
$connection3 = $this->getMockConnection('tcp://127.0.0.1:7003');
|
||||
|
||||
$cluster = new RedisCluster($factory, new Parameters());
|
||||
$cluster->add($connection1);
|
||||
$cluster->add($connection2);
|
||||
$cluster->add($connection3);
|
||||
|
||||
$this->expectException('Predis\NotSupportedException');
|
||||
$this->expectExceptionMessage('Aggregate connections cannot write a raw command buffer');
|
||||
$expectedConnection = $cluster->getConnectionByCommand($command);
|
||||
|
||||
foreach ([$connection1, $connection2, $connection3] as $connection) {
|
||||
if ($connection === $expectedConnection) {
|
||||
$connection->expects($this->once())->method('write')->with($command->serializeCommand());
|
||||
} else {
|
||||
$connection->expects($this->never())->method('write');
|
||||
}
|
||||
}
|
||||
|
||||
$cluster->write($command->serializeCommand());
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression guard for GHSA-w6f5-v2h6-g786 (CWE-93): a CRLF embedded in a
|
||||
* bulk string KEY must not corrupt the argument list used to pick the
|
||||
* target node, which would silently route the command to the wrong node.
|
||||
*
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testWriteHandlesCRLFEmbeddedInBulkStringKey(): void
|
||||
{
|
||||
$command = new Command\Redis\SET();
|
||||
$command->setArguments(["victim\r\n*1\r\n\$4\r\nEVIL", 'somevalue']);
|
||||
|
||||
$factory = $this->getMockBuilder(FactoryInterface::class)->getMock();
|
||||
|
||||
$connection1 = $this->getMockConnection('tcp://127.0.0.1:7001');
|
||||
$connection2 = $this->getMockConnection('tcp://127.0.0.1:7002');
|
||||
$connection3 = $this->getMockConnection('tcp://127.0.0.1:7003');
|
||||
|
||||
$cluster = new RedisCluster($factory, new Parameters());
|
||||
$cluster->add($connection1);
|
||||
$cluster->add($connection2);
|
||||
$cluster->add($connection3);
|
||||
|
||||
$expectedConnection = $cluster->getConnectionByCommand($command);
|
||||
|
||||
foreach ([$connection1, $connection2, $connection3] as $connection) {
|
||||
if ($connection === $expectedConnection) {
|
||||
$connection->expects($this->once())->method('write')->with($command->serializeCommand());
|
||||
} else {
|
||||
$connection->expects($this->never())->method('write');
|
||||
}
|
||||
}
|
||||
|
||||
$cluster->write($command->serializeCommand());
|
||||
}
|
||||
|
||||
@@ -1459,38 +1459,102 @@ repl_backlog_histlen:12978
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression guard for CVE GHSA-w6f5-v2h6-g786 (CWE-93): an aggregate connection
|
||||
* must refuse a raw, already-serialized command buffer instead of re-splitting it
|
||||
* on "\r\n". The old parser ignored RESP bulk-length prefixes, so CRLF sequences
|
||||
* smuggled into a value or key were parsed as extra commands and routed to a node.
|
||||
*
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testWriteRejectsRawCommandBuffer(): void
|
||||
public function testWrite(): void
|
||||
{
|
||||
// A single command whose key carries a smuggled FLUSHDB payload; the old code
|
||||
// would have re-parsed and routed the FLUSHDB, this must route nothing.
|
||||
$command = new Command\Redis\Json\JSONGET();
|
||||
$command->setArguments(["slug:PAD\r\n*1\r\n\$7\r\nFLUSHDB"]);
|
||||
$command1 = new Command\Redis\Json\JSONGET();
|
||||
$command1->setArguments(['arg1']);
|
||||
|
||||
$command2 = new Command\Redis\Json\JSONGET();
|
||||
$command2->setArguments(['arg2']);
|
||||
|
||||
$command3 = new Command\Redis\Json\JSONGET();
|
||||
$command3->setArguments(['arg3']);
|
||||
|
||||
$master = $this->getMockConnection('tcp://127.0.0.1:6379?role=master');
|
||||
$slave1 = $this->getMockConnection('tcp://127.0.0.1:6380?role=slave');
|
||||
|
||||
$master
|
||||
->expects($this->never())
|
||||
->method('write');
|
||||
|
||||
$slave1
|
||||
->expects($this->never())
|
||||
->method('write');
|
||||
|
||||
$master
|
||||
->expects($this->exactly(3))
|
||||
->method('write')
|
||||
->withConsecutive(
|
||||
[$command1->serializeCommand()],
|
||||
[$command2->serializeCommand()],
|
||||
[$command3->serializeCommand()]
|
||||
);
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
|
||||
$replication->add($master);
|
||||
$replication->add($slave1);
|
||||
|
||||
$this->expectException('Predis\NotSupportedException');
|
||||
$this->expectExceptionMessage('Aggregate connections cannot write a raw command buffer');
|
||||
$replication->write($command1->serializeCommand() . $command2->serializeCommand() . $command3->serializeCommand());
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression guard for GHSA-w6f5-v2h6-g786 (CWE-93): a CRLF embedded in a
|
||||
* bulk string's own value must not be mistaken for a command boundary
|
||||
* (splitting one command into a smuggled extra command).
|
||||
*
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testWriteHandlesCRLFEmbeddedInBulkStringValue(): void
|
||||
{
|
||||
$command = new Command\Redis\SET();
|
||||
$command->setArguments(['victim-key', "PAD\r\n*1\r\n\$7\r\nFLUSHDB"]);
|
||||
|
||||
$master = $this->getMockConnection('tcp://127.0.0.1:6379?role=master');
|
||||
$slave1 = $this->getMockConnection('tcp://127.0.0.1:6380?role=slave');
|
||||
|
||||
$slave1
|
||||
->expects($this->never())
|
||||
->method('write');
|
||||
|
||||
$master
|
||||
->expects($this->once())
|
||||
->method('write')
|
||||
->with($command->serializeCommand());
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave1);
|
||||
|
||||
$replication->write($command->serializeCommand());
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression guard for GHSA-w6f5-v2h6-g786 (CWE-93): a CRLF embedded in a
|
||||
* bulk string KEY must not corrupt the argument list used to pick the
|
||||
* target connection, which would silently route the command to the
|
||||
* wrong node.
|
||||
*
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testWriteHandlesCRLFEmbeddedInBulkStringKey(): void
|
||||
{
|
||||
$command = new Command\Redis\SET();
|
||||
$command->setArguments(["victim\r\n*1\r\n\$4\r\nEVIL", 'somevalue']);
|
||||
|
||||
$master = $this->getMockConnection('tcp://127.0.0.1:6379?role=master');
|
||||
$slave1 = $this->getMockConnection('tcp://127.0.0.1:6380?role=slave');
|
||||
|
||||
$slave1
|
||||
->expects($this->never())
|
||||
->method('write');
|
||||
|
||||
$master
|
||||
->expects($this->once())
|
||||
->method('write')
|
||||
->with($command->serializeCommand());
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave1);
|
||||
|
||||
$replication->write($command->serializeCommand());
|
||||
}
|
||||
|
||||
@@ -1975,19 +1975,18 @@ class SentinelReplicationTest extends PredisTestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression guard for CVE GHSA-w6f5-v2h6-g786 (CWE-93): an aggregate connection
|
||||
* must refuse a raw, already-serialized command buffer instead of re-splitting it
|
||||
* on "\r\n". The old parser ignored RESP bulk-length prefixes, so CRLF sequences
|
||||
* smuggled into a value or key were parsed as extra commands and routed to a node.
|
||||
*
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testWriteRejectsRawCommandBuffer(): void
|
||||
public function testWrite(): void
|
||||
{
|
||||
// A single command whose argument carries a smuggled FLUSHDB payload; the old
|
||||
// code would have re-parsed and routed the FLUSHDB, this must route nothing.
|
||||
$command = new Command\Redis\Search\FTSEARCH();
|
||||
$command->setArguments(["idx:PAD\r\n*1\r\n\$7\r\nFLUSHDB", '*']);
|
||||
$command1 = new Command\Redis\Search\FTSEARCH();
|
||||
$command1->setArguments(['arg1', '*']);
|
||||
|
||||
$command2 = new Command\Redis\Search\FTSEARCH();
|
||||
$command2->setArguments(['arg2', '*']);
|
||||
|
||||
$command3 = new Command\Redis\Search\FTSEARCH();
|
||||
$command3->setArguments(['arg3', '*']);
|
||||
|
||||
$sentinel = $this->getMockSentinelConnection('tcp://127.0.0.1:5381?role=sentinel');
|
||||
$master = $this->getMockConnection('tcp://127.0.0.1:6379?role=master');
|
||||
@@ -1996,20 +1995,106 @@ class SentinelReplicationTest extends PredisTestCase
|
||||
$factory = new Connection\Factory();
|
||||
|
||||
$master
|
||||
->expects($this->never())
|
||||
->method('write');
|
||||
->expects($this->exactly(3))
|
||||
->method('isConnected')
|
||||
->willReturn(true);
|
||||
|
||||
$slave
|
||||
->expects($this->never())
|
||||
->method('write');
|
||||
|
||||
$master
|
||||
->expects($this->exactly(3))
|
||||
->method('write')
|
||||
->withConsecutive(
|
||||
[$command1->serializeCommand()],
|
||||
[$command2->serializeCommand()],
|
||||
[$command3->serializeCommand()]
|
||||
);
|
||||
|
||||
$replication = new SentinelReplication('svc', [$sentinel], $factory, $strategy);
|
||||
|
||||
$replication->add($master);
|
||||
$replication->add($slave);
|
||||
|
||||
$this->expectException('Predis\NotSupportedException');
|
||||
$this->expectExceptionMessage('Aggregate connections cannot write a raw command buffer');
|
||||
$replication->write($command1->serializeCommand() . $command2->serializeCommand() . $command3->serializeCommand());
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression guard for GHSA-w6f5-v2h6-g786 (CWE-93): a CRLF embedded in a
|
||||
* bulk string's own value must not be mistaken for a command boundary
|
||||
* (splitting one command into a smuggled extra command).
|
||||
*
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testWriteHandlesCRLFEmbeddedInBulkStringValue(): void
|
||||
{
|
||||
$command = new Command\Redis\SET();
|
||||
$command->setArguments(['victim-key', "PAD\r\n*1\r\n\$7\r\nFLUSHDB"]);
|
||||
|
||||
$sentinel = $this->getMockSentinelConnection('tcp://127.0.0.1:5381?role=sentinel');
|
||||
$master = $this->getMockConnection('tcp://127.0.0.1:6379?role=master');
|
||||
$slave = $this->getMockConnection('tcp://127.0.0.1:6380?role=slave');
|
||||
$strategy = new Replication\ReplicationStrategy();
|
||||
$factory = new Connection\Factory();
|
||||
|
||||
$master
|
||||
->expects($this->once())
|
||||
->method('isConnected')
|
||||
->willReturn(true);
|
||||
|
||||
$slave
|
||||
->expects($this->never())
|
||||
->method('write');
|
||||
|
||||
$master
|
||||
->expects($this->once())
|
||||
->method('write')
|
||||
->with($command->serializeCommand());
|
||||
|
||||
$replication = new SentinelReplication('svc', [$sentinel], $factory, $strategy);
|
||||
$replication->add($master);
|
||||
$replication->add($slave);
|
||||
|
||||
$replication->write($command->serializeCommand());
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression guard for GHSA-w6f5-v2h6-g786 (CWE-93): a CRLF embedded in a
|
||||
* bulk string KEY must not corrupt the argument list used to pick the
|
||||
* target connection, which would silently route the command to the
|
||||
* wrong node.
|
||||
*
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testWriteHandlesCRLFEmbeddedInBulkStringKey(): void
|
||||
{
|
||||
$command = new Command\Redis\SET();
|
||||
$command->setArguments(["victim\r\n*1\r\n\$4\r\nEVIL", 'somevalue']);
|
||||
|
||||
$sentinel = $this->getMockSentinelConnection('tcp://127.0.0.1:5381?role=sentinel');
|
||||
$master = $this->getMockConnection('tcp://127.0.0.1:6379?role=master');
|
||||
$slave = $this->getMockConnection('tcp://127.0.0.1:6380?role=slave');
|
||||
$strategy = new Replication\ReplicationStrategy();
|
||||
$factory = new Connection\Factory();
|
||||
|
||||
$master
|
||||
->expects($this->once())
|
||||
->method('isConnected')
|
||||
->willReturn(true);
|
||||
|
||||
$slave
|
||||
->expects($this->never())
|
||||
->method('write');
|
||||
|
||||
$master
|
||||
->expects($this->once())
|
||||
->method('write')
|
||||
->with($command->serializeCommand());
|
||||
|
||||
$replication = new SentinelReplication('svc', [$sentinel], $factory, $strategy);
|
||||
$replication->add($master);
|
||||
$replication->add($slave);
|
||||
|
||||
$replication->write($command->serializeCommand());
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
namespace Predis\Connection\Resource;
|
||||
|
||||
use ErrorException;
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
@@ -431,6 +432,99 @@ class StreamTest extends TestCase
|
||||
$stream->write('');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testWriteSuppressesEngineWarningUnderThrowingErrorHandler(): void
|
||||
{
|
||||
$this->registerEngineWarningWrapper();
|
||||
$this->installThrowingErrorHandler();
|
||||
|
||||
$stream = new Stream(fopen('predis-test-engine-warning://x', 'r+'));
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Unable to write to stream');
|
||||
|
||||
$stream->write('data');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testReadSuppressesEngineWarningUnderThrowingErrorHandler(): void
|
||||
{
|
||||
$this->registerEngineWarningWrapper();
|
||||
$this->installThrowingErrorHandler();
|
||||
|
||||
$stream = new Stream(fopen('predis-test-engine-warning://x', 'r+'));
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Unable to read from stream');
|
||||
|
||||
// Use the fgets() path (length = -1): PHP 7.2's fread() coerces a
|
||||
// user stream wrapper's `false` return into an empty string instead
|
||||
// of preserving it, which would make this assertion PHP-version
|
||||
// dependent; fgets() doesn't have that quirk.
|
||||
$stream->read(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mimics error handlers installed by Laravel, Symfony and Laminas, which
|
||||
* convert engine notices/warnings into thrown exceptions unless the call
|
||||
* site suppressed them with `@` (see GH-1725). The runner's own ambient
|
||||
* error_reporting() level is irrelevant to what we want to assert here,
|
||||
* so it's pinned to a known, fully-enabled value for the duration of the
|
||||
* test rather than trusted as-is.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function installThrowingErrorHandler(): void
|
||||
{
|
||||
$this->originalErrorReporting = error_reporting(E_ALL);
|
||||
|
||||
set_error_handler(static function ($level, $message, $file = '', $line = 0) {
|
||||
if (error_reporting() & $level) {
|
||||
throw new ErrorException($message, 0, $level, $file, $line);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$this->registeredErrorHandler = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $registeredErrorHandler = false;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $originalErrorReporting;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if ($this->registeredErrorHandler) {
|
||||
restore_error_handler();
|
||||
error_reporting($this->originalErrorReporting);
|
||||
$this->registeredErrorHandler = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private function registerEngineWarningWrapper(): void
|
||||
{
|
||||
if (!in_array('predis-test-engine-warning', stream_get_wrappers(), true)) {
|
||||
stream_wrapper_register('predis-test-engine-warning', EngineWarningStreamWrapperFixture::class);
|
||||
}
|
||||
}
|
||||
|
||||
public function writableModeProvider(): array
|
||||
{
|
||||
return [
|
||||
@@ -478,3 +572,56 @@ class StreamTest extends TestCase
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream wrapper fixture that raises an engine-style warning from
|
||||
* stream_write()/stream_read(), used to verify that Stream::write()/read()
|
||||
* suppress it instead of letting a host-installed error handler turn it
|
||||
* into an uncaught exception (see GH-1725).
|
||||
*/
|
||||
class EngineWarningStreamWrapperFixture
|
||||
{
|
||||
/**
|
||||
* @var resource
|
||||
*/
|
||||
public $context;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$openedPath): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|bool
|
||||
*/
|
||||
public function stream_write(string $data)
|
||||
{
|
||||
trigger_error('fwrite(): synthetic broken pipe', E_USER_WARNING);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|bool
|
||||
*/
|
||||
public function stream_read(int $count)
|
||||
{
|
||||
trigger_error('fread(): synthetic broken pipe', E_USER_WARNING);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_eof(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function stream_close(): void
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,19 +67,6 @@ class Resp3StrategyTest extends PredisTestCase
|
||||
$this->assertSame($expectedValue, $actualResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider nanProvider
|
||||
* @group disconnected
|
||||
* @param string $data
|
||||
* @return void
|
||||
*/
|
||||
public function testParseDataReturnsFloatNanOnNanValue(string $data): void
|
||||
{
|
||||
$actualResponse = $this->strategy->parseData($data);
|
||||
|
||||
$this->assertNan($actualResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider booleanProvider
|
||||
* @group disconnected
|
||||
@@ -184,16 +171,6 @@ class Resp3StrategyTest extends PredisTestCase
|
||||
];
|
||||
}
|
||||
|
||||
public function nanProvider(): array
|
||||
{
|
||||
return [
|
||||
'canonical nan' => [",nan\r\n"],
|
||||
'negative nan' => [",-nan\r\n"],
|
||||
'uppercase nan' => [",NAN\r\n"],
|
||||
'nan with payload' => [",nan(ind)\r\n"],
|
||||
];
|
||||
}
|
||||
|
||||
public function booleanProvider(): array
|
||||
{
|
||||
return [
|
||||
|
||||
Reference in New Issue
Block a user