diff --git a/docs/changelogs/v4.7.7.md b/docs/changelogs/v4.7.7.md index cdf4eb2db148..3a1512dec526 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\Forge::_alterTable()` interpolated table and column names without escaping in default constraint lookups. - **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/Forge.php b/system/Database/SQLSRV/Forge.php index fa29fe5dbd75..da438357b4ef 100644 --- a/system/Database/SQLSRV/Forge.php +++ b/system/Database/SQLSRV/Forge.php @@ -195,7 +195,8 @@ protected function _alterTable(string $alterType, string $table, $processedField } } - $fullTable = $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table); + $fullTable = $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table); + $escapedFullTable = $this->db->escape($fullTable); // Drop default constraints $fields = implode(',', $this->db->escape((array) $columnNamesToDrop)); @@ -203,9 +204,9 @@ protected function _alterTable(string $alterType, string $table, $processedField $sql = <<db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table); - $colName = $field['name']; // bare, for sys.columns lookup + $fullTable = $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table); + $escapedFullTable = $this->db->escape($fullTable); + $escapedColName = $this->db->escape($field['name']); // find the existing default constraint name for this column $findSql = <<db->query($findSql)->getRowArray(); diff --git a/tests/system/Database/Forge/SQLSRVForgeTest.php b/tests/system/Database/Forge/SQLSRVForgeTest.php new file mode 100644 index 000000000000..3621a3ce024f --- /dev/null +++ b/tests/system/Database/Forge/SQLSRVForgeTest.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Database\Forge; + +use CodeIgniter\Database\ResultInterface; +use CodeIgniter\Database\SQLSRV\Connection; +use CodeIgniter\Database\SQLSRV\Forge; +use CodeIgniter\Database\SQLSRV\Result; +use CodeIgniter\Test\CIUnitTestCase; +use PHPUnit\Framework\Attributes\Group; + +/** + * @internal + */ +#[Group('Others')] +final class SQLSRVForgeTest extends CIUnitTestCase +{ + public function testModifyColumnEscapesTableNameAndColumnNameInDefaultConstraintLookup(): void + { + $db = new class () extends Connection { + /** + * @var list + */ + public array $queries = []; + + public function __construct() + { + $this->DBDriver = 'SQLSRV'; + $this->schema = 'dbo'; + $this->escapeChar = ['[', ']']; + $this->connID = false; + $this->pretend = true; + } + + public function initialize(): void + { + } + + /** + * @param array|string|null $binds + */ + public function query(string $sql, $binds = null, bool $setEscapeFlags = true, string $queryClass = ''): ResultInterface + { + $this->queries[] = $sql; + + return new class ($this->connID, $this->resultID) extends Result { + public function getRowArray(int $n = 0): array + { + return ['constraint_name' => 'df_test']; + } + }; + } + }; + + $forge = new Forge($db); + + $forge->modifyColumn("user's", [ + "col'name" => [ + 'type' => 'VARCHAR', + 'default' => "'default_val'", + ], + ]); + + $this->assertNotEmpty($db->queries); + $findSql = $db->queries[0]; + + $this->assertStringContainsString("OBJECT_ID(N'[dbo].[user''s]')", $findSql); + $this->assertStringContainsString("c.name = N'col''name'", $findSql); + } + + public function testDropColumnEscapesTableNameInDefaultConstraintLookup(): void + { + $db = new class () extends Connection { + /** + * @var list + */ + public array $queries = []; + + public function __construct() + { + $this->DBDriver = 'SQLSRV'; + $this->schema = 'dbo'; + $this->escapeChar = ['[', ']']; + $this->connID = false; + $this->pretend = true; + } + + public function initialize(): void + { + } + + /** + * @param array|string|null $binds + */ + public function query(string $sql, $binds = null, bool $setEscapeFlags = true, string $queryClass = ''): ResultInterface + { + $this->queries[] = $sql; + + return new class ($this->connID, $this->resultID) extends Result { + public function getResultArray(): array + { + return []; + } + }; + } + }; + + $forge = new Forge($db); + + $forge->dropColumn("user's", "col'name"); + + $this->assertCount(3, $db->queries); + $dropDefaultSql = $db->queries[1]; + + $this->assertStringContainsString("parent_object_id = OBJECT_ID(N'[dbo].[user''s]')", $dropDefaultSql); + $this->assertStringContainsString("object_id = OBJECT_ID(N'[dbo].[user''s]')", $dropDefaultSql); + } +}