From 191a9b9d933216da159152eacf6e80380950b211 Mon Sep 17 00:00:00 2001 From: sander-hash Date: Thu, 23 Apr 2026 13:38:31 +0200 Subject: [PATCH 1/2] Added escape for scalar string --- src/Transport/Http.php | 3 ++ tests/NativeParamsTest.php | 56 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/Transport/Http.php b/src/Transport/Http.php index 2d22210..9ce4991 100644 --- a/src/Transport/Http.php +++ b/src/Transport/Http.php @@ -865,6 +865,9 @@ private function convertParamValue(mixed $value): string if ($value === null) { return '\\N'; } + if (is_string($value)) { + return str_replace(['\\', "'"], ['\\\\', "\\'"], $value); + } return (string) $value; } diff --git a/tests/NativeParamsTest.php b/tests/NativeParamsTest.php index 7750679..c9803eb 100644 --- a/tests/NativeParamsTest.php +++ b/tests/NativeParamsTest.php @@ -155,6 +155,62 @@ public function testSelectWithStringArrayParamInjectionAttempt(): void $this->assertEquals(["x','injected','y"], $result->fetchOne('arr')); } + public function testSelectWithStringParamTrailingBackslash(): void + { + $result = $this->client->selectWithParams( + 'SELECT {search:String} as search', + ['search' => 'hello\\'] + ); + + $this->assertEquals('hello\\', $result->fetchOne('search')); + } + + public function testSelectWithStringParamEmbeddedBackslash(): void + { + $result = $this->client->selectWithParams( + 'SELECT {search:String} as search', + ['search' => 'a\\b\\c'] + ); + + $this->assertEquals('a\\b\\c', $result->fetchOne('search')); + } + + public function testSelectWithStringParamSingleQuote(): void + { + $result = $this->client->selectWithParams( + 'SELECT {name:String} as name', + ['name' => "O'Brien"] + ); + + $this->assertEquals("O'Brien", $result->fetchOne('name')); + } + + public function testSelectWithStringParamInjectionAttempt(): void + { + $result = $this->client->selectWithParams( + 'SELECT {val:String} as val', + ['val' => "x','injected','y"] + ); + + $this->assertEquals("x','injected','y", $result->fetchOne('val')); + } + + public function testWriteWithStringParamTrailingBackslash(): void + { + $this->client->write("DROP TABLE IF EXISTS string_param_escape_test"); + $this->client->write('CREATE TABLE IF NOT EXISTS string_param_escape_test (id UInt32, val String) ENGINE = Memory'); + + $this->client->writeWithParams( + 'INSERT INTO string_param_escape_test VALUES ({id:UInt32}, {val:String})', + ['id' => 1, 'val' => 'hello\\'] + ); + + $st = $this->client->select('SELECT val FROM string_param_escape_test WHERE id = 1'); + $this->assertEquals('hello\\', $st->fetchOne('val')); + + $this->client->write("DROP TABLE IF EXISTS string_param_escape_test"); + } + public function testSelectWithPerQuerySettings(): void { $result = $this->client->selectWithParams( From 8610c7c95c5341c282d880544451f2a537f630d0 Mon Sep 17 00:00:00 2001 From: sander-hash Date: Thu, 23 Apr 2026 13:49:36 +0200 Subject: [PATCH 2/2] Call function recursive instead --- src/Transport/Http.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transport/Http.php b/src/Transport/Http.php index 9ce4991..db00f45 100644 --- a/src/Transport/Http.php +++ b/src/Transport/Http.php @@ -854,7 +854,7 @@ private function convertParamValue(mixed $value): string $arrayValues = []; foreach ($value as $val) { if (is_string($val)) { - $escaped = str_replace(['\\', "'"], ['\\\\', "\\'"], $val); + $escaped = $this->convertParamValue($val); $arrayValues[] = sprintf("'%s'", $escaped); continue; }