Compare commits

..

1 Commits

Author SHA1 Message Date
Danny de Wit ad1850b110 fix: keep TableExtractor table names as strings (#1794)
PHP coerces numeric-string array keys to ints, so collecting table names in
$tables[$name] and returning array_keys() can yield ints. That breaks the
declared list<string> and throws a TypeError in Recorder::linkTable(string).

Standard SQL like substring(x FROM 1 FOR 10) is enough to trigger it, since
the numeric operand matches the FROM pattern.

Closes #1793

Co-authored-by: Danny de Wit <d.dewit@fenceweb.com>
2026-08-02 22:50:47 -04:00
2 changed files with 19 additions and 2 deletions
+2 -2
View File
@@ -56,7 +56,7 @@ final class TableExtractor
$tables[strtolower($name)] = true;
}
$out = array_keys($tables);
$out = array_map(strval(...), array_keys($tables));
sort($out);
return $out;
@@ -112,7 +112,7 @@ final class TableExtractor
}
}
$out = array_keys($tables);
$out = array_map(strval(...), array_keys($tables));
sort($out);
return $out;
+17
View File
@@ -47,6 +47,15 @@ describe('fromSql()', function (): void {
->and(TableExtractor::fromSql('select * from information_schema.tables'))->toBeEmpty();
});
it('does not leak int keys for numeric identifiers', function (): void {
// `substring(x FROM 1 FOR 3)` is standard SQL, and the `1` matches the
// FROM pattern. Collecting names as array keys makes PHP coerce the
// numeric string to an int, which then violates the declared
// list<string> and blows up Recorder::linkTable(string).
expect(TableExtractor::fromSql('select substring(name from 1 for 3) from users'))
->each->toBeString();
});
it('returns nothing for non-DML statements', function (): void {
expect(TableExtractor::fromSql('PRAGMA foreign_keys = ON'))->toBeEmpty()
->and(TableExtractor::fromSql(''))->toBeEmpty()
@@ -89,6 +98,14 @@ describe('fromMigrationSource()', function (): void {
->toBe(['audits', 'events', 'sessions', 'settings', 'users']);
});
it('does not leak int keys for numeric table names', function (): void {
// A table named `123` is a legal quoted identifier. Collecting names as
// array keys makes PHP coerce it to an int, breaking the declared
// list<string>, so it must survive as a string rather than be dropped.
expect(TableExtractor::fromMigrationSource("DB::table('123')->insert([]);"))
->toBe(['123']);
});
it('extracts tables from DB::table calls', function (): void {
expect(TableExtractor::fromMigrationSource("DB::table('permissions')->insert([]);"))
->toBe(['permissions']);