1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-08-06 17:14:04 +00:00

Parameter validation and other DB manager improvements (#1459)

### Parameter validation

In `statement()` calls of `TenantDatabaseManager`s, use parameter
binding when possible. When that's not possible, validate the parameters
using `validateParameter()` or `validatePassword()`.

Passwords use a less strict allowlist than other parameters (e.g. DB
names), since passwords tend to use more special characters but we can
afford to be more restrictive in the generic `validateParameter()`.

In `SQLiteDatabaseManager`, names of file-based databases are validated
(in `createDatabase`, `deleteDatabase` and `databaseExists`) using a
similar allowlist to the (non-password) parameters in other DB managers,
with an additional character: `.` (this addition is necessary since
file-based SQLite databases end with `.sqlite`).

### DatabaseTenancyBootstrapper - harden() and the lost test file

While checking for more places that could use validation, I realized
that it's possible to update tenant's db_name to the central DB or the
DB of another tenant. Added the `DatabaseTenancyBootstrapper::$harden`
property -- setting it to true prevents tenants from connecting to the
wrong databases (`RuntimeException` is thrown after connecting to the
wrong database).

Also, the DatabaseTenancyBootstrapper test file was ignored while
running tests because it lacked the `Test` suffix. Added the suffix and
fixed the broken `DATABASE_URL` test in the file.

### SQLiteDatabaseManager - respect static $path property in
makeConnectionConfig()

SQLiteDatabaseManager had a bug in `makeConnectionConfig`: the method
didn't respect the static `$path` property, it used `database_path()`
instead. Added a regression test for that. Also recognizing in-memory
SQLite databases (using `isInMemory()`) is more strict now so that
simply having a db_name with `_tenancy_inmemory_` somewhere in the name
doesn't make a file-based database considered in-memory.

### MySQLDatabaseManager - charset and collation defaulting

Creating databases with `null` charsets and collations resulted in a
`QueryException`, since null isn't a valid charset/collation. To solve
that, in the `CREATE DATABASE` statement in MySQLDatabaseManager, only
add charset/collation to the statement if they are not null.

MySQL defaults to the server's charset and collation, so it's safe to
not pass any charset/collation in the `CREATE DATABASE` statement and
let MySQL choose. Also, if e.g. collation is non-null and charset is
null, MySQL will use a charset compatible with the used collation, and
this works both ways.

---------

Co-authored-by: Samuel Stancl <samuel@archte.ch>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
lukinovec 2026-06-28 04:51:30 +02:00 committed by GitHub
parent ecf031237d
commit aa9d1d7fcf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 695 additions and 61 deletions

View file

@ -5,14 +5,42 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Bootstrappers;
use Exception;
use Illuminate\Support\Facades\Schema;
use RuntimeException;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Database\DatabaseManager;
use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException;
use Throwable;
class DatabaseTenancyBootstrapper implements TenancyBootstrapper
{
/**
* When true, throw an exception if a tenant gets connected to
* another tenant's database or to the central database.
*
* This case should never come up in well-configured apps where
* users cannot set or edit tenant IDs or database names, so this
* option is disabled by default.
*
* However, applications dealing with extremely sensitive data may
* choose to enable this runtime check to prevent a bug or misconfiguration
* from creating an exploit that would let an attacker access another
* tenant's data or data from the central database.
*
* One way such a scenario might come up is if an application allows
* broad tenant attribute updates on a page for updating some fields
* on the tenant, without restricting that action to only a limited
* set of fields that are safe to edit. An attacker might be able to add
* something like ['tenancy_db_name' => '...'] to the request which could
* lead to this internal attribute being updated on an existing tenant.
*
* It's possible that enabling this setting will negate the performance
* benefits of cached tenant lookup.
*/
public static bool $harden = false;
/** @var DatabaseManager */
protected $database;
@ -41,10 +69,39 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper
}
$this->database->connectToTenant($tenant);
if (static::$harden) {
try {
$this->verifyTenantCanUseDatabase($tenant);
} catch (Throwable $e) {
// Revert connection back to central
$this->revert();
throw $e;
}
}
}
public function revert(): void
{
$this->database->reconnectToCentral();
}
protected function verifyTenantCanUseDatabase(Tenant $tenant): void
{
/** @var \Stancl\Tenancy\Database\Models\Tenant&TenantWithDatabase $tenant */
$tenantDbName = $tenant->database()->getName();
// Check that no other tenant uses this tenant's database
if ($tenant::where($tenant->getTenantKeyName(), '!=', $tenant->getTenantKey())
->where($tenant::getDataColumn() . '->' . $tenant->internalPrefix() . 'db_name', $tenantDbName)
->exists()) {
throw new RuntimeException('Tenant cannot use a database of another tenant.');
}
if (Schema::hasTable($tenant->getTable())) {
// Throw if the current database/schema has the tenants table (i.e. it's not central)
throw new RuntimeException('Tenant cannot use the central database.');
}
}
}