From bceba748f3a600a9c3e14495884725018f8c8225 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 5 Nov 2026 22:07:49 +0100 Subject: [PATCH] fix(Database): escape table string in SQLSRV Builder::_upsertBatch() Object_ID --- docs/changelogs/v4.7.7.md | 1 + system/Database/SQLSRV/Builder.php | 2 +- .../Database/Builder/SQLSRVBuilderTest.php | 46 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/system/Database/Builder/SQLSRVBuilderTest.php diff --git a/docs/changelogs/v4.7.7.md b/docs/changelogs/v4.7.7.md index cdf4eb2db148..28203acdf4c7 100644 --- a/docs/changelogs/v4.7.7.md +++ b/docs/changelogs/v4.7.7.md @@ -14,6 +14,7 @@ Release Date: Unreleased ## Bugs Fixed +- **Database:** Fixed a bug where `SQLSRV\Builder::_upsertBatch()` concatenated `$table` unescaped into `Object_ID()`, causing syntax errors for table names with quotes or special characters. - **Security:** Fixed a bug where `Security::__construct()` threw a `TypeError` when the CSRF cookie was an array, causing an HTTP 500 response for affected requests. - **Worker Mode:** Fixed a bug where every request that ended with an uncaught exception, including a `404 Page Not Found` without a 404 override, called `exit()` in the exception handler. This ended the worker script, so FrankenPHP booted a new one, and the worker loop skipped its cleanup after the request. The exception handler now returns to the worker loop. diff --git a/system/Database/SQLSRV/Builder.php b/system/Database/SQLSRV/Builder.php index d98cbe0d9d0b..356db0686892 100644 --- a/system/Database/SQLSRV/Builder.php +++ b/system/Database/SQLSRV/Builder.php @@ -685,7 +685,7 @@ protected function _upsertBatch(string $table, array $keys, array $values): stri $constraints = $this->QBOptions['constraints'] ?? []; $tableIdentity = $this->QBOptions['tableIdentity'] ?? ''; - $sql = "SELECT name from syscolumns where id = Object_ID('" . $table . "') and colstat = 1"; + $sql = 'SELECT name from syscolumns where id = Object_ID(' . $this->db->escape($table) . ') and colstat = 1'; if (($query = $this->db->query($sql)) === false) { throw new DatabaseException('Failed to get table identity'); } diff --git a/tests/system/Database/Builder/SQLSRVBuilderTest.php b/tests/system/Database/Builder/SQLSRVBuilderTest.php new file mode 100644 index 000000000000..1dfc6782d9e1 --- /dev/null +++ b/tests/system/Database/Builder/SQLSRVBuilderTest.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Database\Builder; + +use CodeIgniter\Database\SQLSRV\Builder; +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\Mock\MockConnection; +use PHPUnit\Framework\Attributes\Group; +use ReflectionMethod; +use Throwable; + +/** + * @internal + */ +#[Group('Others')] +final class SQLSRVBuilderTest extends CIUnitTestCase +{ + public function testUpsertBatchEscapesTableNameInObjectID(): void + { + $db = new MockConnection(['DBDriver' => 'SQLSRV']); + $builder = new Builder("user's", $db); + + $method = new ReflectionMethod($builder, '_upsertBatch'); + + try { + $method->invoke($builder, "user's", ['id', 'name'], [['1', 'Alice']]); + } catch (Throwable) { + } + + $this->assertSame( + "SELECT name from syscolumns where id = Object_ID('user''s') and colstat = 1", + (string) $db->getLastQuery(), + ); + } +}