diff --git a/src/Adapter/Driver/Pdo/Statement.php b/src/Adapter/Driver/Pdo/Statement.php index 7894b432..71abff23 100644 --- a/src/Adapter/Driver/Pdo/Statement.php +++ b/src/Adapter/Driver/Pdo/Statement.php @@ -22,6 +22,8 @@ use function is_array; use function is_int; use function ltrim; +use function preg_match; +use function sprintf; class Statement implements StatementInterface, PdoDriverAwareInterface, ProfilerAwareInterface { @@ -223,8 +225,19 @@ protected function bindParametersFromContainer(): void }; } - // parameter is named or positional, value is reference - $parameter = is_int($name) ? $name + 1 : ':' . ltrim($name, ':'); + if (is_int($name)) { + $parameter = $name + 1; + } else { + if (! preg_match('/^:?[a-zA-Z0-9_]+$/', $name)) { + throw new Exception\RuntimeException(sprintf( + 'The PDO param "%s" contains invalid characters.' + . ' Only alphabetic characters, digits, and underscores (_) are allowed.', + $name + )); + } + $parameter = ':' . ltrim($name, ':'); + } + $this->resource->bindParam($parameter, $value, $type); } diff --git a/test/unit/Adapter/Driver/Pdo/StatementTest.php b/test/unit/Adapter/Driver/Pdo/StatementTest.php index c049e7cc..c95ba6b4 100644 --- a/test/unit/Adapter/Driver/Pdo/StatementTest.php +++ b/test/unit/Adapter/Driver/Pdo/StatementTest.php @@ -7,10 +7,12 @@ use Override; use PhpDb\Adapter\Driver\Pdo\Result; use PhpDb\Adapter\Driver\Pdo\Statement; +use PhpDb\Adapter\Exception\RuntimeException; use PhpDb\Adapter\ParameterContainer; use PhpDbTest\Adapter\Driver\Pdo\TestAsset\TestConnection; use PhpDbTest\Adapter\Driver\Pdo\TestAsset\TestPdo; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; #[CoversMethod(Statement::class, 'setDriver')] @@ -22,6 +24,7 @@ #[CoversMethod(Statement::class, 'prepare')] #[CoversMethod(Statement::class, 'isPrepared')] #[CoversMethod(Statement::class, 'execute')] +#[CoversMethod(Statement::class, 'bindParametersFromContainer')] final class StatementTest extends TestCase { protected Statement $statement; @@ -111,4 +114,29 @@ public function testExecute(): void $this->statement->prepare('SELECT 1'); self::assertInstanceOf(Result::class, $this->statement->execute()); } + + /** @return array */ + public static function invalidParameterNameProvider(): array + { + return [ + 'dollar sign' => ['tz$'], + 'with colon' => [':tz$'], + 'hyphen' => ['my-param'], + 'space' => ['my param'], + 'dot' => ['my.param'], + 'at sign' => ['param@name'], + ]; + } + + #[DataProvider('invalidParameterNameProvider')] + public function testExecuteThrowsOnInvalidParameterName(string $name): void + { + $this->statement->setDriver(new TestPdo(new TestConnection($pdo = new TestAsset\SqliteMemoryPdo()))); + $this->statement->initialize($pdo); + $this->statement->prepare('SELECT 1'); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('contains invalid characters'); + $this->statement->execute([$name => 'value']); + } }