coolify/tests/Feature/MassAssignmentProtectionTest.php
Andras Bacsai 67a4fcc2ab fix: add mass assignment protection to models
Replace $guarded = [] with explicit $fillable whitelists across all
models. Update controllers to use request->only($allowedFields) when
assigning request data. Switch Livewire components to forceFill() for
explicit mass assignment. Add integration tests for mass assignment
protection.
2026-03-28 12:32:57 +01:00

182 lines
7.3 KiB
PHP

<?php
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use App\Models\Team;
use App\Models\User;
describe('mass assignment protection', function () {
test('no API-exposed model uses unguarded $guarded = []', function () {
$models = [
Application::class,
Service::class,
User::class,
Team::class,
Server::class,
StandalonePostgresql::class,
StandaloneRedis::class,
StandaloneMysql::class,
StandaloneMariadb::class,
StandaloneMongodb::class,
StandaloneKeydb::class,
StandaloneDragonfly::class,
StandaloneClickhouse::class,
];
foreach ($models as $modelClass) {
$model = new $modelClass;
$guarded = $model->getGuarded();
$fillable = $model->getFillable();
// Model must NOT have $guarded = [] (empty guard = no protection)
// It should either have non-empty $guarded OR non-empty $fillable
$hasProtection = $guarded !== ['*'] ? count($guarded) > 0 : true;
$hasProtection = $hasProtection || count($fillable) > 0;
expect($hasProtection)
->toBeTrue("Model {$modelClass} has no mass assignment protection (empty \$guarded and empty \$fillable)");
}
});
test('Application model blocks mass assignment of relationship IDs', function () {
$application = new Application;
$dangerousFields = ['id', 'uuid', 'environment_id', 'destination_id', 'destination_type', 'source_id', 'source_type', 'private_key_id', 'repository_project_id'];
foreach ($dangerousFields as $field) {
expect($application->isFillable($field))
->toBeFalse("Application model should not allow mass assignment of '{$field}'");
}
});
test('Application model allows mass assignment of user-facing fields', function () {
$application = new Application;
$userFields = ['name', 'description', 'git_repository', 'git_branch', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'health_check_path', 'limits_memory', 'status'];
foreach ($userFields as $field) {
expect($application->isFillable($field))
->toBeTrue("Application model should allow mass assignment of '{$field}'");
}
});
test('Server model has $fillable and no conflicting $guarded', function () {
$server = new Server;
$fillable = $server->getFillable();
$guarded = $server->getGuarded();
expect($fillable)->not->toBeEmpty('Server model should have explicit $fillable');
// Guarded should be the default ['*'] when $fillable is set, not []
expect($guarded)->not->toBe([], 'Server model should not have $guarded = [] overriding $fillable');
});
test('Server model blocks mass assignment of dangerous fields', function () {
$server = new Server;
// These fields should not be mass-assignable via the API
expect($server->isFillable('id'))->toBeFalse();
expect($server->isFillable('uuid'))->toBeFalse();
expect($server->isFillable('created_at'))->toBeFalse();
});
test('User model blocks mass assignment of auth-sensitive fields', function () {
$user = new User;
expect($user->isFillable('id'))->toBeFalse('User id should not be fillable');
expect($user->isFillable('email_verified_at'))->toBeFalse('email_verified_at should not be fillable');
expect($user->isFillable('remember_token'))->toBeFalse('remember_token should not be fillable');
expect($user->isFillable('two_factor_secret'))->toBeFalse('two_factor_secret should not be fillable');
expect($user->isFillable('two_factor_recovery_codes'))->toBeFalse('two_factor_recovery_codes should not be fillable');
});
test('User model allows mass assignment of profile fields', function () {
$user = new User;
expect($user->isFillable('name'))->toBeTrue();
expect($user->isFillable('email'))->toBeTrue();
expect($user->isFillable('password'))->toBeTrue();
});
test('Team model blocks mass assignment of internal fields', function () {
$team = new Team;
expect($team->isFillable('id'))->toBeFalse();
expect($team->isFillable('personal_team'))->toBeFalse('personal_team should not be fillable');
});
test('standalone database models block mass assignment of relationship IDs', function () {
$models = [
StandalonePostgresql::class,
StandaloneRedis::class,
StandaloneMysql::class,
StandaloneMariadb::class,
StandaloneMongodb::class,
StandaloneKeydb::class,
StandaloneDragonfly::class,
StandaloneClickhouse::class,
];
foreach ($models as $modelClass) {
$model = new $modelClass;
$dangerousFields = ['id', 'uuid', 'environment_id', 'destination_id', 'destination_type'];
foreach ($dangerousFields as $field) {
expect($model->isFillable($field))
->toBeFalse("Model {$modelClass} should not allow mass assignment of '{$field}'");
}
}
});
test('standalone database models allow mass assignment of config fields', function () {
$model = new StandalonePostgresql;
expect($model->isFillable('name'))->toBeTrue();
expect($model->isFillable('postgres_user'))->toBeTrue();
expect($model->isFillable('postgres_password'))->toBeTrue();
expect($model->isFillable('image'))->toBeTrue();
expect($model->isFillable('limits_memory'))->toBeTrue();
$model = new StandaloneRedis;
expect($model->isFillable('redis_password'))->toBeTrue();
$model = new StandaloneMysql;
expect($model->isFillable('mysql_root_password'))->toBeTrue();
$model = new StandaloneMongodb;
expect($model->isFillable('mongo_initdb_root_username'))->toBeTrue();
});
test('Application fill ignores non-fillable fields', function () {
$application = new Application;
$application->fill([
'name' => 'test-app',
'environment_id' => 999,
'destination_id' => 999,
'team_id' => 999,
'private_key_id' => 999,
]);
expect($application->name)->toBe('test-app');
expect($application->environment_id)->toBeNull();
expect($application->destination_id)->toBeNull();
expect($application->private_key_id)->toBeNull();
});
test('Service model blocks mass assignment of relationship IDs', function () {
$service = new Service;
expect($service->isFillable('id'))->toBeFalse();
expect($service->isFillable('uuid'))->toBeFalse();
expect($service->isFillable('environment_id'))->toBeFalse();
expect($service->isFillable('destination_id'))->toBeFalse();
expect($service->isFillable('server_id'))->toBeFalse();
});
});