Skip to content

Commit cc882d6

Browse files
authored
fix: strip the table prefix correctly when rebuilding a SQLite3 table (#10509)
SQLite cannot alter or drop a column in place, so `SQLite3\Table` rebuilds the whole table and recreates its foreign keys from the metadata it collected. That metadata holds prefixed table names, and `createTable()` stripped the prefix with `trim($name, $this->db->DBPrefix)`. `trim()`'s second argument is a set of characters, not a prefix. It removes any of those characters from either end of the string, repeatedly, so with the prefix `db_` it turns `db_bandit_fk` into `andit_fk` — the leading `b` of the table's own name is eaten as well, and characters are stripped from the end too. The rebuilt table's foreign keys then reference tables that do not exist. Nothing fails at that point, because foreign key enforcement is off for the duration of the rebuild; the error surfaces at the next write to the referenced table, naming a table that appears nowhere in the schema. Whether a given table is affected depends on which characters its name happens to begin and end with, so most tables come through untouched. Strip the prefix the way `fromTable()` in the same class already does. The existing tests could not catch this: `AlterTableTest` builds its own connection without a `DBPrefix`, and the damage is invisible until the constraint is used. The regression test therefore sets a prefix and names the referenced table so that it begins with a character the prefix also contains, which is what makes the bug reproduce.
1 parent 860b5ec commit cc882d6

3 files changed

Lines changed: 71 additions & 1 deletion

File tree

system/Database/SQLite3/Table.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,9 +333,16 @@ protected function createTable()
333333
}
334334

335335
foreach ($this->foreignKeys as $foreignKey) {
336+
$foreignTableName = $foreignKey->foreign_table_name;
337+
$prefix = $this->db->DBPrefix;
338+
339+
if ($prefix !== '' && str_starts_with($foreignTableName, $prefix)) {
340+
$foreignTableName = substr($foreignTableName, strlen($prefix));
341+
}
342+
336343
$this->forge->addForeignKey(
337344
$foreignKey->column_name,
338-
trim($foreignKey->foreign_table_name, $this->db->DBPrefix),
345+
$foreignTableName,
339346
$foreignKey->foreign_column_name,
340347
);
341348
}

tests/system/Database/Live/SQLite3/AlterTableTest.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,68 @@ public function testProcessCopiesOldData(): void
271271
$this->seeInDatabase('foo', ['email' => 'funkalicious@example.com']);
272272
}
273273

274+
public function testDropColumnKeepsForeignKeyTableNameWhenPrefixIsSet(): void
275+
{
276+
$config = [
277+
'DBDriver' => 'SQLite3',
278+
'database' => ':memory:',
279+
'DBDebug' => true,
280+
'DBPrefix' => 'db_',
281+
];
282+
283+
$db = db_connect($config, false);
284+
$this->assertInstanceOf(Connection::class, $db);
285+
286+
$forge = Database::forge($db);
287+
$this->assertInstanceOf(Forge::class, $forge);
288+
289+
// The referenced table must begin with a character that also appears in
290+
// the prefix, or the prefix stripping cannot damage its name.
291+
$forge->addField([
292+
'id' => [
293+
'type' => 'integer',
294+
'constraint' => 11,
295+
'unsigned' => true,
296+
'auto_increment' => true,
297+
],
298+
]);
299+
$forge->addPrimaryKey('id');
300+
$forge->createTable('bandit_fk');
301+
302+
$forge->addField([
303+
'id' => [
304+
'type' => 'integer',
305+
'constraint' => 11,
306+
'unsigned' => true,
307+
'auto_increment' => true,
308+
],
309+
'key_id' => [
310+
'type' => 'integer',
311+
'constraint' => 11,
312+
'unsigned' => true,
313+
],
314+
'name' => [
315+
'type' => 'varchar',
316+
'constraint' => 255,
317+
'null' => true,
318+
],
319+
]);
320+
$forge->addPrimaryKey('id');
321+
$forge->addForeignKey('key_id', 'bandit_fk', 'id');
322+
$forge->createTable('bandit');
323+
324+
// Dropping a column rebuilds the table, recreating its foreign keys.
325+
$this->assertTrue($forge->dropColumn('bandit', 'name'));
326+
327+
$keys = array_values($db->getForeignKeyData('bandit'));
328+
329+
$this->assertCount(1, $keys);
330+
$this->assertSame($db->DBPrefix . 'bandit_fk', $keys[0]->foreign_table_name);
331+
332+
$forge->dropTable('bandit', true);
333+
$forge->dropTable('bandit_fk', true);
334+
}
335+
274336
protected function createTable(string $tableName = 'foo'): void
275337
{
276338
// Create support table for foreign keys

user_guide_src/source/changelogs/v4.7.5.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Bugs Fixed
4242
- **CodeIgniter:** Fixed a bug where ``gatherOutput()`` could be called twice when ``startController()`` returned a ``ResponseInterface`` (e.g., from filter attributes or closure routes).
4343
- **Content Security Policy:** Fixed a bug where empty ``Content-Security-Policy``, ``Content-Security-Policy-Report-Only``, and ``Reporting-Endpoints`` response headers were generated when no corresponding values existed.
4444
- **Cookie:** Fixed a bug where ``Cookie`` instances created with ``raw: true`` allowed invalid characters in cookie values rejected by ``setrawcookie()``.
45+
- **Database:** Fixed a bug where rebuilding a SQLite3 table (e.g., ``Forge::dropColumn()``, ``Forge::modifyColumn()``, ``Forge::dropForeignKey()`` and ``Forge::dropPrimaryKey()``) corrupted the table names referenced by its foreign keys when ``DBPrefix`` was set.
4546
- **Files:** Fixed a bug where ``File::move()`` and ``UploadedFile::move()`` set executable and overly permissive file permissions (``0777 & ~umask()`` instead of ``0666 & ~umask()``), and ``UploadedFile::move()`` targeted the parent directory instead of the destination file for ``chmod()``.
4647
- **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them.
4748
- **Honeypot:** Fixed a bug where bot detection returned an HTTP 500 response instead of 403 (Forbidden).

0 commit comments

Comments
 (0)