fix(models): replace forceFill/forceCreate with fill/create and add fillable guards

Replace all uses of `forceFill`, `forceCreate`, and `forceFill` with their
non-force equivalents across models, actions, controllers, and Livewire
components. Add explicit `$fillable` arrays to all affected Eloquent models
to enforce mass assignment protection.

Add ModelFillableCreationTest and ModelFillableRegressionTest to verify that
model creation respects fillable constraints and prevent regressions.
This commit is contained in:
Andras Bacsai
2026-03-31 13:45:31 +02:00
parent 047aff1c82
commit 1a603a10ed
79 changed files with 1411 additions and 142 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ class ResetUserPassword implements ResetsUserPasswords
'password' => ['required', Password::defaults(), 'confirmed'],
])->validate();
$user->forceFill([
$user->fill([
'password' => Hash::make($input['password']),
])->save();
$user->deleteAllSessions();
+1 -1
View File
@@ -24,7 +24,7 @@ class UpdateUserPassword implements UpdatesUserPasswords
'current_password.current_password' => __('The provided password does not match your current password.'),
])->validateWithBag('updatePassword');
$user->forceFill([
$user->fill([
'password' => Hash::make($input['password']),
])->save();
}
@@ -35,7 +35,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
) {
$this->updateVerifiedUser($user, $input);
} else {
$user->forceFill([
$user->fill([
'name' => $input['name'],
'email' => $input['email'],
])->save();
@@ -49,7 +49,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
*/
protected function updateVerifiedUser(User $user, array $input): void
{
$user->forceFill([
$user->fill([
'name' => $input['name'],
'email' => $input['email'],
'email_verified_at' => null,
+1 -1
View File
@@ -49,7 +49,7 @@ class InstallDocker
}');
$found = StandaloneDocker::where('server_id', $server->id);
if ($found->count() == 0 && $server->id) {
StandaloneDocker::forceCreate([
StandaloneDocker::create([
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $server->id,
+1 -1
View File
@@ -136,7 +136,7 @@ class Emails extends Command
$application = Application::all()->first();
$preview = ApplicationPreview::all()->first();
if (! $preview) {
$preview = ApplicationPreview::forceCreate([
$preview = ApplicationPreview::create([
'application_id' => $application->id,
'pull_request_id' => 1,
'pull_request_html_url' => 'http://example.com',
@@ -258,7 +258,7 @@ class ProjectController extends Controller
], 422);
}
$project = Project::forceCreate([
$project = Project::create([
'name' => $request->name,
'description' => $request->description,
'team_id' => $teamId,
@@ -432,7 +432,7 @@ class ServicesController extends Controller
if (in_array($oneClickServiceName, NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK)) {
data_set($servicePayload, 'connect_to_docker_network', true);
}
$service = Service::forceCreate($servicePayload);
$service = Service::create($servicePayload);
$service->name = $request->name ?? "$oneClickServiceName-".$service->uuid;
$service->description = $request->description;
if ($request->has('is_container_label_escape_enabled')) {
+2 -2
View File
@@ -119,7 +119,7 @@ class Bitbucket extends Controller
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
if ($application->build_pack === 'dockercompose') {
$pr_app = ApplicationPreview::forceCreate([
$pr_app = ApplicationPreview::create([
'git_type' => 'bitbucket',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
@@ -128,7 +128,7 @@ class Bitbucket extends Controller
]);
$pr_app->generate_preview_fqdn_compose();
} else {
$pr_app = ApplicationPreview::forceCreate([
$pr_app = ApplicationPreview::create([
'git_type' => 'bitbucket',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
+2 -2
View File
@@ -144,7 +144,7 @@ class Gitea extends Controller
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
if ($application->build_pack === 'dockercompose') {
$pr_app = ApplicationPreview::forceCreate([
$pr_app = ApplicationPreview::create([
'git_type' => 'gitea',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
@@ -153,7 +153,7 @@ class Gitea extends Controller
]);
$pr_app->generate_preview_fqdn_compose();
} else {
$pr_app = ApplicationPreview::forceCreate([
$pr_app = ApplicationPreview::create([
'git_type' => 'gitea',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
+2 -2
View File
@@ -177,7 +177,7 @@ class Gitlab extends Controller
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
if ($application->build_pack === 'dockercompose') {
$pr_app = ApplicationPreview::forceCreate([
$pr_app = ApplicationPreview::create([
'git_type' => 'gitlab',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
@@ -186,7 +186,7 @@ class Gitlab extends Controller
]);
$pr_app->generate_preview_fqdn_compose();
} else {
$pr_app = ApplicationPreview::forceCreate([
$pr_app = ApplicationPreview::create([
'git_type' => 'gitlab',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
+2 -2
View File
@@ -118,7 +118,7 @@ class ProcessGithubPullRequestWebhook implements ShouldBeEncrypted, ShouldQueue
if (! $found) {
if ($application->build_pack === 'dockercompose') {
$preview = ApplicationPreview::forceCreate([
$preview = ApplicationPreview::create([
'git_type' => 'github',
'application_id' => $application->id,
'pull_request_id' => $this->pullRequestId,
@@ -127,7 +127,7 @@ class ProcessGithubPullRequestWebhook implements ShouldBeEncrypted, ShouldQueue
]);
$preview->generate_preview_fqdn_compose();
} else {
$preview = ApplicationPreview::forceCreate([
$preview = ApplicationPreview::create([
'git_type' => 'github',
'application_id' => $application->id,
'pull_request_id' => $this->pullRequestId,
+1 -1
View File
@@ -441,7 +441,7 @@ class Index extends Component
public function createNewProject()
{
$this->createdProject = Project::forceCreate([
$this->createdProject = Project::create([
'name' => 'My first project',
'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2,
+2 -2
View File
@@ -77,7 +77,7 @@ class Docker extends Component
if ($found) {
throw new \Exception('Network already added to this server.');
} else {
$docker = SwarmDocker::forceCreate([
$docker = SwarmDocker::create([
'name' => $this->name,
'network' => $this->network,
'server_id' => $this->selectedServer->id,
@@ -88,7 +88,7 @@ class Docker extends Component
if ($found) {
throw new \Exception('Network already added to this server.');
} else {
$docker = StandaloneDocker::forceCreate([
$docker = StandaloneDocker::create([
'name' => $this->name,
'network' => $this->network,
'server_id' => $this->selectedServer->id,
+1 -1
View File
@@ -48,7 +48,7 @@ class ForcePasswordReset extends Component
$this->rateLimit(10);
$this->validate();
$firstLogin = auth()->user()->created_at == auth()->user()->updated_at;
auth()->user()->forceFill([
auth()->user()->fill([
'password' => Hash::make($this->password),
'force_password_reset' => false,
])->save();
+1 -1
View File
@@ -30,7 +30,7 @@ class AddEmpty extends Component
{
try {
$this->validate();
$project = Project::forceCreate([
$project = Project::create([
'name' => $this->name,
'description' => $this->description,
'team_id' => currentTeam()->id,
@@ -196,7 +196,7 @@ class Previews extends Component
$this->setDeploymentUuid();
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found && ! is_null($pull_request_html_url)) {
$found = ApplicationPreview::forceCreate([
$found = ApplicationPreview::create([
'application_id' => $this->application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url,
@@ -210,7 +210,7 @@ class Previews extends Component
$this->setDeploymentUuid();
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found && (! is_null($pull_request_html_url) || ($this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()))) {
$found = ApplicationPreview::forceCreate([
$found = ApplicationPreview::create([
'application_id' => $this->application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url ?? '',
@@ -262,7 +262,7 @@ class Previews extends Component
$this->setDeploymentUuid();
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found && (! is_null($pull_request_html_url) || ($this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()))) {
$found = ApplicationPreview::forceCreate([
$found = ApplicationPreview::create([
'application_id' => $this->application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url ?? '',
+16 -16
View File
@@ -100,7 +100,7 @@ class CloneMe extends Component
if ($foundProject) {
throw new \Exception('Project with the same name already exists.');
}
$project = Project::forceCreate([
$project = Project::create([
'name' => $this->newName,
'team_id' => currentTeam()->id,
'description' => $this->project->description.' (clone)',
@@ -139,7 +139,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => $uuid,
'status' => 'exited',
'started_at' => null,
@@ -188,7 +188,7 @@ class CloneMe extends Component
'created_at',
'updated_at',
'uuid',
])->forceFill([
])->fill([
'name' => $newName,
'resource_id' => $newDatabase->id,
]);
@@ -217,7 +217,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'resource_id' => $newDatabase->id,
]);
$newStorage->save();
@@ -230,7 +230,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => $uuid,
'database_id' => $newDatabase->id,
'database_type' => $newDatabase->getMorphClass(),
@@ -248,7 +248,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->forceFill($payload);
])->fill($payload);
$newEnvironmentVariable->save();
}
}
@@ -259,7 +259,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => $uuid,
'environment_id' => $environment->id,
'destination_id' => $this->selectedDestination,
@@ -277,7 +277,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => (string) new Cuid2,
'service_id' => $newService->id,
'team_id' => currentTeam()->id,
@@ -291,7 +291,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'resourceable_id' => $newService->id,
'resourceable_type' => $newService->getMorphClass(),
]);
@@ -299,7 +299,7 @@ class CloneMe extends Component
}
foreach ($newService->applications() as $application) {
$application->forceFill([
$application->fill([
'status' => 'exited',
])->save();
@@ -317,7 +317,7 @@ class CloneMe extends Component
'created_at',
'updated_at',
'uuid',
])->forceFill([
])->fill([
'name' => $newName,
'resource_id' => $application->id,
]);
@@ -346,7 +346,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'resource_id' => $application->id,
]);
$newStorage->save();
@@ -354,7 +354,7 @@ class CloneMe extends Component
}
foreach ($newService->databases() as $database) {
$database->forceFill([
$database->fill([
'status' => 'exited',
])->save();
@@ -372,7 +372,7 @@ class CloneMe extends Component
'created_at',
'updated_at',
'uuid',
])->forceFill([
])->fill([
'name' => $newName,
'resource_id' => $database->id,
]);
@@ -401,7 +401,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'resource_id' => $database->id,
]);
$newStorage->save();
@@ -414,7 +414,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => $uuid,
'database_id' => $database->id,
'database_type' => $database->getMorphClass(),
+1 -1
View File
@@ -54,7 +54,7 @@ class DockerCompose extends Component
}
$destination_class = $destination->getMorphClass();
$service = Service::forceCreate([
$service = Service::create([
'docker_compose_raw' => $this->dockerComposeRaw,
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
+1 -1
View File
@@ -133,7 +133,7 @@ class DockerImage extends Component
// Determine the image tag based on whether it's a hash or regular tag
$imageTag = $parser->isImageHash() ? 'sha256-'.$parser->getTag() : $parser->getTag();
$application = Application::forceCreate([
$application = Application::create([
'name' => 'docker-image-'.new Cuid2,
'repository_project_id' => 0,
'git_repository' => 'coollabsio/coolify',
+1 -1
View File
@@ -10,7 +10,7 @@ class EmptyProject extends Component
{
public function createEmptyProject()
{
$project = Project::forceCreate([
$project = Project::create([
'name' => generate_random_name(),
'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2,
@@ -191,7 +191,7 @@ class GithubPrivateRepository extends Component
$project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
$environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
$application = Application::forceCreate([
$application = Application::create([
'name' => generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name),
'repository_project_id' => $this->selected_repository_id,
'git_repository' => str($this->selected_repository_owner)->trim()->toString().'/'.str($this->selected_repository_repo)->trim()->toString(),
@@ -183,7 +183,7 @@ class GithubPrivateRepositoryDeployKey extends Component
$application_init['docker_compose_location'] = $this->docker_compose_location;
$application_init['base_directory'] = $this->base_directory;
}
$application = Application::forceCreate($application_init);
$application = Application::create($application_init);
$application->settings->is_static = $this->is_static;
$application->settings->save();
@@ -299,7 +299,7 @@ class PublicGitRepository extends Component
$new_service['source_id'] = $this->git_source->id;
$new_service['source_type'] = $this->git_source->getMorphClass();
}
$service = Service::forceCreate($new_service);
$service = Service::create($new_service);
return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid,
@@ -346,7 +346,7 @@ class PublicGitRepository extends Component
$application_init['docker_compose_location'] = $this->docker_compose_location;
$application_init['base_directory'] = $this->base_directory;
}
$application = Application::forceCreate($application_init);
$application = Application::create($application_init);
$application->settings->is_static = $this->isStatic;
$application->settings->save();
@@ -52,7 +52,7 @@ CMD ["nginx", "-g", "daemon off;"]
if (! $port) {
$port = 80;
}
$application = Application::forceCreate([
$application = Application::create([
'name' => 'dockerfile-'.new Cuid2,
'repository_project_id' => 0,
'git_repository' => 'coollabsio/coolify',
+1 -1
View File
@@ -91,7 +91,7 @@ class Create extends Component
if (in_array($oneClickServiceName, NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK)) {
data_set($service_payload, 'connect_to_docker_network', true);
}
$service = Service::forceCreate($service_payload);
$service = Service::create($service_payload);
$service->name = "$oneClickServiceName-".$service->uuid;
$service->save();
if ($oneClickDotEnvs?->count() > 0) {
@@ -94,7 +94,7 @@ class ResourceOperations extends Component
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
'status' => 'exited',
@@ -143,7 +143,7 @@ class ResourceOperations extends Component
'created_at',
'updated_at',
'uuid',
])->forceFill([
])->fill([
'name' => $newName,
'resource_id' => $new_resource->id,
]);
@@ -172,7 +172,7 @@ class ResourceOperations extends Component
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'resource_id' => $new_resource->id,
]);
$newStorage->save();
@@ -185,7 +185,7 @@ class ResourceOperations extends Component
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => $uuid,
'database_id' => $new_resource->id,
'database_type' => $new_resource->getMorphClass(),
@@ -204,7 +204,7 @@ class ResourceOperations extends Component
'id',
'created_at',
'updated_at',
])->forceFill($payload);
])->fill($payload);
$newEnvironmentVariable->save();
}
@@ -221,7 +221,7 @@ class ResourceOperations extends Component
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
'destination_id' => $new_destination->id,
@@ -242,7 +242,7 @@ class ResourceOperations extends Component
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => (string) new Cuid2,
'service_id' => $new_resource->id,
'team_id' => currentTeam()->id,
@@ -256,7 +256,7 @@ class ResourceOperations extends Component
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'resourceable_id' => $new_resource->id,
'resourceable_type' => $new_resource->getMorphClass(),
]);
@@ -264,7 +264,7 @@ class ResourceOperations extends Component
}
foreach ($new_resource->applications() as $application) {
$application->forceFill([
$application->fill([
'status' => 'exited',
])->save();
@@ -282,7 +282,7 @@ class ResourceOperations extends Component
'created_at',
'updated_at',
'uuid',
])->forceFill([
])->fill([
'name' => $newName,
'resource_id' => $application->id,
]);
@@ -307,7 +307,7 @@ class ResourceOperations extends Component
}
foreach ($new_resource->databases() as $database) {
$database->forceFill([
$database->fill([
'status' => 'exited',
])->save();
@@ -325,7 +325,7 @@ class ResourceOperations extends Component
'created_at',
'updated_at',
'uuid',
])->forceFill([
])->fill([
'name' => $newName,
'resource_id' => $database->id,
]);
@@ -366,7 +366,7 @@ class ResourceOperations extends Component
try {
$this->authorize('update', $this->resource);
$new_environment = Environment::ownedByCurrentTeam()->findOrFail($environment_id);
$this->resource->forceFill([
$this->resource->fill([
'environment_id' => $environment_id,
])->save();
if ($this->resource->type() === 'application') {
+1 -1
View File
@@ -42,7 +42,7 @@ class Show extends Component
{
try {
$this->validate();
$environment = Environment::forceCreate([
$environment = Environment::create([
'name' => $this->name,
'project_id' => $this->project->id,
'uuid' => (string) new Cuid2,
+2 -2
View File
@@ -43,7 +43,7 @@ class Destinations extends Component
return;
} else {
SwarmDocker::forceCreate([
SwarmDocker::create([
'name' => $this->server->name.'-'.$name,
'network' => $this->name,
'server_id' => $this->server->id,
@@ -57,7 +57,7 @@ class Destinations extends Component
return;
} else {
StandaloneDocker::forceCreate([
StandaloneDocker::create([
'name' => $this->server->name.'-'.$name,
'network' => $name,
'server_id' => $this->server->id,
+10 -2
View File
@@ -203,6 +203,14 @@ class Application extends BaseModel
'restart_count',
'last_restart_at',
'last_restart_type',
'uuid',
'environment_id',
'destination_id',
'destination_type',
'source_id',
'source_type',
'repository_project_id',
'private_key_id',
];
protected $appends = ['server_status'];
@@ -262,7 +270,7 @@ class Application extends BaseModel
}
}
if (count($payload) > 0) {
$application->forceFill($payload);
$application->fill($payload);
}
// Buildpack switching cleanup logic
@@ -299,7 +307,7 @@ class Application extends BaseModel
}
});
static::created(function ($application) {
ApplicationSetting::forceCreate([
ApplicationSetting::create([
'application_id' => $application->id,
]);
$application->compose_parsing_version = self::$parserVersion;
@@ -44,6 +44,7 @@ class ApplicationDeploymentQueue extends Model
'application_id',
'deployment_uuid',
'pull_request_id',
'docker_registry_image_tag',
'force_rebuild',
'commit',
'status',
+2 -1
View File
@@ -11,6 +11,7 @@ class ApplicationPreview extends BaseModel
use SoftDeletes;
protected $fillable = [
'uuid',
'application_id',
'pull_request_id',
'pull_request_html_url',
@@ -62,7 +63,7 @@ class ApplicationPreview extends BaseModel
});
static::saving(function ($preview) {
if ($preview->isDirty('status')) {
$preview->forceFill(['last_online_at' => now()]);
$preview->last_online_at = now();
}
});
}
+1
View File
@@ -29,6 +29,7 @@ class ApplicationSetting extends Model
];
protected $fillable = [
'application_id',
'is_static',
'is_git_submodules_enabled',
'is_git_lfs_enabled',
+1
View File
@@ -5,6 +5,7 @@ namespace App\Models;
class CloudProviderToken extends BaseModel
{
protected $fillable = [
'team_id',
'provider',
'token',
'name',
+2
View File
@@ -28,6 +28,8 @@ class Environment extends BaseModel
protected $fillable = [
'name',
'description',
'project_id',
'uuid',
];
protected static function booted()
+2
View File
@@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
class GithubApp extends BaseModel
{
protected $fillable = [
'team_id',
'private_key_id',
'name',
'organization',
'api_url',
+4 -2
View File
@@ -27,6 +27,8 @@ class Project extends BaseModel
protected $fillable = [
'name',
'description',
'team_id',
'uuid',
];
/**
@@ -51,10 +53,10 @@ class Project extends BaseModel
protected static function booted()
{
static::created(function ($project) {
ProjectSetting::forceCreate([
ProjectSetting::create([
'project_id' => $project->id,
]);
Environment::forceCreate([
Environment::create([
'name' => 'production',
'project_id' => $project->id,
'uuid' => (string) new Cuid2,
+3 -1
View File
@@ -6,7 +6,9 @@ use Illuminate\Database\Eloquent\Model;
class ProjectSetting extends Model
{
protected $fillable = [];
protected $fillable = [
'project_id',
];
public function project()
{
+2
View File
@@ -9,6 +9,8 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
class ScheduledDatabaseBackup extends BaseModel
{
protected $fillable = [
'uuid',
'team_id',
'description',
'enabled',
'save_s3',
@@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ScheduledDatabaseBackupExecution extends BaseModel
{
protected $fillable = [
'uuid',
'scheduled_database_backup_id',
'status',
'message',
'size',
+4
View File
@@ -30,12 +30,16 @@ class ScheduledTask extends BaseModel
use HasSafeStringAttribute;
protected $fillable = [
'uuid',
'enabled',
'name',
'command',
'frequency',
'container',
'timeout',
'team_id',
'application_id',
'service_id',
];
public static function ownedByCurrentTeamAPI(int $teamId)
+1
View File
@@ -23,6 +23,7 @@ use OpenApi\Attributes as OA;
class ScheduledTaskExecution extends BaseModel
{
protected $fillable = [
'scheduled_task_id',
'status',
'message',
'finished_at',
+2 -1
View File
@@ -135,7 +135,7 @@ class Server extends BaseModel
$payload['ip_previous'] = $server->getOriginal('ip');
}
}
$server->forceFill($payload);
$server->fill($payload);
});
static::saved(function ($server) {
if ($server->wasChanged('private_key_id') || $server->privateKey?->isDirty()) {
@@ -265,6 +265,7 @@ class Server extends BaseModel
'detected_traefik_version',
'traefik_outdated_info',
'server_metadata',
'ip_previous',
];
use HasSafeStringAttribute;
+1
View File
@@ -54,6 +54,7 @@ use OpenApi\Attributes as OA;
class ServerSetting extends Model
{
protected $fillable = [
'server_id',
'is_swarm_manager',
'is_jump_server',
'is_build_server',
+5
View File
@@ -49,6 +49,7 @@ class Service extends BaseModel
private static $parserVersion = '5';
protected $fillable = [
'uuid',
'name',
'description',
'docker_compose_raw',
@@ -58,6 +59,10 @@ class Service extends BaseModel
'config_hash',
'compose_parsing_version',
'is_container_label_escape_enabled',
'environment_id',
'server_id',
'destination_id',
'destination_type',
];
protected $appends = ['server_status', 'status'];
+2 -1
View File
@@ -12,6 +12,7 @@ class ServiceApplication extends BaseModel
use HasFactory, SoftDeletes;
protected $fillable = [
'service_id',
'name',
'human_name',
'description',
@@ -39,7 +40,7 @@ class ServiceApplication extends BaseModel
});
static::saving(function ($service) {
if ($service->isDirty('status')) {
$service->forceFill(['last_online_at' => now()]);
$service->last_online_at = now();
}
});
}
+2 -1
View File
@@ -10,6 +10,7 @@ class ServiceDatabase extends BaseModel
use HasFactory, SoftDeletes;
protected $fillable = [
'service_id',
'name',
'human_name',
'description',
@@ -44,7 +45,7 @@ class ServiceDatabase extends BaseModel
});
static::saving(function ($service) {
if ($service->isDirty('status')) {
$service->forceFill(['last_online_at' => now()]);
$service->last_online_at = now();
}
});
}
+5 -1
View File
@@ -14,6 +14,7 @@ class StandaloneClickhouse extends BaseModel
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
'name',
'description',
'clickhouse_admin_user',
@@ -40,6 +41,9 @@ class StandaloneClickhouse extends BaseModel
'public_port_timeout',
'custom_docker_run_options',
'clickhouse_db',
'destination_type',
'destination_id',
'environment_id',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
@@ -71,7 +75,7 @@ class StandaloneClickhouse extends BaseModel
});
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
$database->last_online_at = now();
}
});
}
+1
View File
@@ -13,6 +13,7 @@ class StandaloneDocker extends BaseModel
use HasSafeStringAttribute;
protected $fillable = [
'server_id',
'name',
'network',
];
+5 -1
View File
@@ -14,6 +14,7 @@ class StandaloneDragonfly extends BaseModel
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
'name',
'description',
'dragonfly_password',
@@ -39,6 +40,9 @@ class StandaloneDragonfly extends BaseModel
'public_port_timeout',
'enable_ssl',
'custom_docker_run_options',
'destination_type',
'destination_id',
'environment_id',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
@@ -70,7 +74,7 @@ class StandaloneDragonfly extends BaseModel
});
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
$database->last_online_at = now();
}
});
}
+5 -1
View File
@@ -14,6 +14,7 @@ class StandaloneKeydb extends BaseModel
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
'name',
'description',
'keydb_password',
@@ -40,6 +41,9 @@ class StandaloneKeydb extends BaseModel
'public_port_timeout',
'enable_ssl',
'custom_docker_run_options',
'destination_type',
'destination_id',
'environment_id',
];
protected $appends = ['internal_db_url', 'external_db_url', 'server_status'];
@@ -71,7 +75,7 @@ class StandaloneKeydb extends BaseModel
});
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
$database->last_online_at = now();
}
});
}
+5 -1
View File
@@ -15,6 +15,7 @@ class StandaloneMariadb extends BaseModel
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
'name',
'description',
'mariadb_root_password',
@@ -43,6 +44,9 @@ class StandaloneMariadb extends BaseModel
'enable_ssl',
'is_log_drain_enabled',
'custom_docker_run_options',
'destination_type',
'destination_id',
'environment_id',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
@@ -74,7 +78,7 @@ class StandaloneMariadb extends BaseModel
});
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
$database->last_online_at = now();
}
});
}
+5 -1
View File
@@ -14,6 +14,7 @@ class StandaloneMongodb extends BaseModel
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
'name',
'description',
'mongo_conf',
@@ -43,6 +44,9 @@ class StandaloneMongodb extends BaseModel
'is_log_drain_enabled',
'is_include_timestamps',
'custom_docker_run_options',
'destination_type',
'destination_id',
'environment_id',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
@@ -80,7 +84,7 @@ class StandaloneMongodb extends BaseModel
});
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
$database->last_online_at = now();
}
});
}
+5 -1
View File
@@ -14,6 +14,7 @@ class StandaloneMysql extends BaseModel
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
'name',
'description',
'mysql_root_password',
@@ -44,6 +45,9 @@ class StandaloneMysql extends BaseModel
'is_log_drain_enabled',
'is_include_timestamps',
'custom_docker_run_options',
'destination_type',
'destination_id',
'environment_id',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
@@ -76,7 +80,7 @@ class StandaloneMysql extends BaseModel
});
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
$database->last_online_at = now();
}
});
}
+5 -1
View File
@@ -14,6 +14,7 @@ class StandalonePostgresql extends BaseModel
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
'name',
'description',
'postgres_user',
@@ -46,6 +47,9 @@ class StandalonePostgresql extends BaseModel
'is_log_drain_enabled',
'is_include_timestamps',
'custom_docker_run_options',
'destination_type',
'destination_id',
'environment_id',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
@@ -92,7 +96,7 @@ class StandalonePostgresql extends BaseModel
});
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
$database->last_online_at = now();
}
});
}
+5 -1
View File
@@ -14,6 +14,7 @@ class StandaloneRedis extends BaseModel
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
'name',
'description',
'redis_conf',
@@ -39,6 +40,9 @@ class StandaloneRedis extends BaseModel
'is_log_drain_enabled',
'is_include_timestamps',
'custom_docker_run_options',
'destination_type',
'destination_id',
'environment_id',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
@@ -69,7 +73,7 @@ class StandaloneRedis extends BaseModel
});
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
$database->last_online_at = now();
}
});
+1
View File
@@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Model;
class Subscription extends Model
{
protected $fillable = [
'team_id',
'stripe_invoice_paid',
'stripe_subscription_id',
'stripe_customer_id',
+1
View File
@@ -7,6 +7,7 @@ use App\Support\ValidationPatterns;
class SwarmDocker extends BaseModel
{
protected $fillable = [
'server_id',
'name',
'network',
];
+1
View File
@@ -10,6 +10,7 @@ class Tag extends BaseModel
protected $fillable = [
'name',
'team_id',
];
protected function customizeName($value)
+4 -1
View File
@@ -49,6 +49,9 @@ class User extends Authenticatable implements SendsEmail
'password',
'force_password_reset',
'marketing_emails',
'pending_email',
'email_change_code',
'email_change_code_expires_at',
];
protected $hidden = [
@@ -409,7 +412,7 @@ class User extends Authenticatable implements SendsEmail
$expiryMinutes = config('constants.email_change.verification_code_expiry_minutes', 10);
$expiresAt = Carbon::now()->addMinutes($expiryMinutes);
$this->forceFill([
$this->fill([
'pending_email' => $newEmail,
'email_change_code' => $code,
'email_change_code_expires_at' => $expiresAt,
+8 -8
View File
@@ -214,7 +214,7 @@ function clone_application(Application $source, $destination, array $overrides =
'updated_at',
'additional_servers_count',
'additional_networks_count',
])->forceFill(array_merge([
])->fill(array_merge([
'uuid' => $uuid,
'name' => $name,
'fqdn' => $url,
@@ -237,7 +237,7 @@ function clone_application(Application $source, $destination, array $overrides =
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'application_id' => $newApplication->id,
]);
$newApplicationSettings->save();
@@ -257,7 +257,7 @@ function clone_application(Application $source, $destination, array $overrides =
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => (string) new Cuid2,
'application_id' => $newApplication->id,
'team_id' => currentTeam()->id,
@@ -272,7 +272,7 @@ function clone_application(Application $source, $destination, array $overrides =
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => (string) new Cuid2,
'application_id' => $newApplication->id,
'status' => 'exited',
@@ -304,7 +304,7 @@ function clone_application(Application $source, $destination, array $overrides =
'created_at',
'updated_at',
'uuid',
])->forceFill([
])->fill([
'name' => $newName,
'resource_id' => $newApplication->id,
]);
@@ -340,7 +340,7 @@ function clone_application(Application $source, $destination, array $overrides =
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'resource_id' => $newApplication->id,
]);
$newStorage->save();
@@ -354,7 +354,7 @@ function clone_application(Application $source, $destination, array $overrides =
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'resourceable_id' => $newApplication->id,
'resourceable_type' => $newApplication->getMorphClass(),
'is_preview' => false,
@@ -371,7 +371,7 @@ function clone_application(Application $source, $destination, array $overrides =
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'resourceable_id' => $newApplication->id,
'resourceable_type' => $newApplication->getMorphClass(),
'is_preview' => true,
+2 -2
View File
@@ -1597,7 +1597,7 @@ function serviceParser(Service $resource): Collection
if ($databaseFound) {
$savedService = $databaseFound;
} else {
$savedService = ServiceDatabase::forceCreate([
$savedService = ServiceDatabase::create([
'name' => $serviceName,
'service_id' => $resource->id,
]);
@@ -1607,7 +1607,7 @@ function serviceParser(Service $resource): Collection
if ($applicationFound) {
$savedService = $applicationFound;
} else {
$savedService = ServiceApplication::forceCreate([
$savedService = ServiceApplication::create([
'name' => $serviceName,
'service_id' => $resource->id,
]);
+4 -4
View File
@@ -1919,7 +1919,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
// Create new serviceApplication or serviceDatabase
if ($isDatabase) {
if ($isNew) {
$savedService = ServiceDatabase::forceCreate([
$savedService = ServiceDatabase::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id,
@@ -1930,7 +1930,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'service_id' => $resource->id,
])->first();
if (is_null($savedService)) {
$savedService = ServiceDatabase::forceCreate([
$savedService = ServiceDatabase::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id,
@@ -1939,7 +1939,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
} else {
if ($isNew) {
$savedService = ServiceApplication::forceCreate([
$savedService = ServiceApplication::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id,
@@ -1950,7 +1950,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'service_id' => $resource->id,
])->first();
if (is_null($savedService)) {
$savedService = ServiceApplication::forceCreate([
$savedService = ServiceApplication::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id,
@@ -31,7 +31,7 @@ beforeEach(function () {
);
});
$this->project = Project::forceCreate([
$this->project = Project::create([
'uuid' => (string) new Cuid2,
'name' => 'test-project',
'team_id' => $this->team->id,
+1 -1
View File
@@ -6,7 +6,7 @@ use App\Models\ApplicationSetting;
describe('Application Rollback', function () {
beforeEach(function () {
$this->application = new Application;
$this->application->forceFill([
$this->application->fill([
'uuid' => 'test-app-uuid',
'git_commit_sha' => 'HEAD',
]);
@@ -31,7 +31,7 @@ beforeEach(function () {
'redirect' => 'both',
]);
$this->application->settings->forceFill([
$this->application->settings->fill([
'is_container_label_readonly_enabled' => false,
])->save();
@@ -92,7 +92,7 @@ test('cloning application with multiple persistent volumes generates unique uuid
});
test('cloning application reassigns settings to the cloned application', function () {
$this->application->settings->forceFill([
$this->application->settings->fill([
'is_static' => true,
'is_spa' => true,
'is_build_server_enabled' => true,
@@ -118,7 +118,7 @@ test('cloning application reassigns settings to the cloned application', functio
});
test('cloning application reassigns scheduled tasks and previews to the cloned application', function () {
$scheduledTask = ScheduledTask::forceCreate([
$scheduledTask = ScheduledTask::create([
'uuid' => 'scheduled-task-original',
'application_id' => $this->application->id,
'team_id' => $this->team->id,
@@ -129,7 +129,7 @@ test('cloning application reassigns scheduled tasks and previews to the cloned a
'timeout' => 120,
]);
$preview = ApplicationPreview::forceCreate([
$preview = ApplicationPreview::create([
'uuid' => 'preview-original',
'application_id' => $this->application->id,
'pull_request_id' => 123,
+3 -3
View File
@@ -14,7 +14,7 @@ it('populates fqdn from docker_compose_domains after generate_preview_fqdn_compo
]),
]);
$preview = ApplicationPreview::forceCreate([
$preview = ApplicationPreview::create([
'application_id' => $application->id,
'pull_request_id' => 42,
'pull_request_html_url' => 'https://github.com/example/repo/pull/42',
@@ -39,7 +39,7 @@ it('populates fqdn with multiple domains from multiple services', function () {
]),
]);
$preview = ApplicationPreview::forceCreate([
$preview = ApplicationPreview::create([
'application_id' => $application->id,
'pull_request_id' => 7,
'pull_request_html_url' => 'https://github.com/example/repo/pull/7',
@@ -65,7 +65,7 @@ it('sets fqdn to null when no domains are configured', function () {
]),
]);
$preview = ApplicationPreview::forceCreate([
$preview = ApplicationPreview::create([
'application_id' => $application->id,
'pull_request_id' => 99,
'pull_request_html_url' => 'https://github.com/example/repo/pull/99',
@@ -33,7 +33,7 @@ beforeEach(function () {
function createDatabase($context): StandalonePostgresql
{
return StandalonePostgresql::forceCreate([
return StandalonePostgresql::create([
'name' => 'test-postgres',
'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres',
@@ -33,7 +33,7 @@ beforeEach(function () {
describe('PATCH /api/v1/databases', function () {
test('updates public_port_timeout on a postgresql database', function () {
$database = StandalonePostgresql::forceCreate([
$database = StandalonePostgresql::create([
'name' => 'test-postgres',
'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres',
@@ -57,7 +57,7 @@ describe('PATCH /api/v1/databases', function () {
});
test('updates public_port_timeout on a redis database', function () {
$database = StandaloneRedis::forceCreate([
$database = StandaloneRedis::create([
'name' => 'test-redis',
'image' => 'redis:7',
'redis_password' => 'password',
@@ -79,7 +79,7 @@ describe('PATCH /api/v1/databases', function () {
});
test('rejects invalid public_port_timeout value', function () {
$database = StandalonePostgresql::forceCreate([
$database = StandalonePostgresql::create([
'name' => 'test-postgres',
'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres',
@@ -101,7 +101,7 @@ describe('PATCH /api/v1/databases', function () {
});
test('accepts null public_port_timeout', function () {
$database = StandalonePostgresql::forceCreate([
$database = StandalonePostgresql::create([
'name' => 'test-postgres',
'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres',
@@ -52,7 +52,7 @@ it('reloads the mysql database model when refreshing so ssl controls follow the
$project = Project::factory()->create(['team_id' => $this->team->id]);
$environment = Environment::factory()->create(['project_id' => $project->id]);
$database = StandaloneMysql::forceCreate([
$database = StandaloneMysql::create([
'name' => 'test-mysql',
'image' => 'mysql:8',
'mysql_root_password' => 'password',
@@ -70,7 +70,7 @@ it('reloads the mysql database model when refreshing so ssl controls follow the
$component = Livewire::test(MysqlGeneral::class, ['database' => $database])
->assertDontSee('Database should be stopped to change this settings.');
$database->forceFill(['status' => 'running:healthy'])->save();
$database->fill(['status' => 'running:healthy'])->save();
$component->call('refresh')
->assertSee('Database should be stopped to change this settings.');
@@ -69,7 +69,7 @@ describe('GetLogs locked properties', function () {
describe('GetLogs Livewire action validation', function () {
test('getLogs rejects invalid container name', function () {
// Make server functional by setting settings directly
$this->server->settings->forceFill([
$this->server->settings->fill([
'is_reachable' => true,
'is_usable' => true,
'force_disabled' => false,
@@ -100,7 +100,7 @@ describe('GetLogs Livewire action validation', function () {
});
test('downloadAllLogs returns empty for invalid container name', function () {
$this->server->settings->forceFill([
$this->server->settings->fill([
'is_reachable' => true,
'is_usable' => true,
'force_disabled' => false,
@@ -31,7 +31,7 @@ beforeEach(function () {
'team_id' => $this->team->id,
]);
$this->githubApp = GithubApp::forceCreate([
$this->githubApp = GithubApp::create([
'name' => 'Test GitHub App',
'api_url' => 'https://api.github.com',
'html_url' => 'https://github.com',
@@ -24,7 +24,7 @@ it('creates application settings for internally created applications', function
]);
$destination = $server->standaloneDockers()->firstOrFail();
$application = Application::forceCreate([
$application = Application::create([
'name' => 'internal-app',
'git_repository' => 'https://github.com/coollabsio/coolify',
'git_branch' => 'main',
@@ -57,7 +57,7 @@ it('creates services with protected relationship ids in trusted internal paths',
]);
$destination = $server->standaloneDockers()->firstOrFail();
$service = Service::forceCreate([
$service = Service::create([
'docker_compose_raw' => 'services: {}',
'environment_id' => $environment->id,
'server_id' => $server->id,
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -14,18 +14,18 @@ uses(RefreshDatabase::class);
it('returns the correct team through the service relationship chain', function () {
$team = Team::factory()->create();
$project = Project::forceCreate([
$project = Project::create([
'uuid' => (string) Str::uuid(),
'name' => 'Test Project',
'team_id' => $team->id,
]);
$environment = Environment::forceCreate([
$environment = Environment::create([
'name' => 'test-env-'.Str::random(8),
'project_id' => $project->id,
]);
$service = Service::forceCreate([
$service = Service::create([
'uuid' => (string) Str::uuid(),
'name' => 'supabase',
'environment_id' => $environment->id,
@@ -34,7 +34,7 @@ it('returns the correct team through the service relationship chain', function (
'docker_compose_raw' => 'version: "3"',
]);
$serviceDatabase = ServiceDatabase::forceCreate([
$serviceDatabase = ServiceDatabase::create([
'uuid' => (string) Str::uuid(),
'name' => 'supabase-db',
'service_id' => $service->id,
@@ -47,18 +47,18 @@ it('returns the correct team through the service relationship chain', function (
it('returns the correct team for ServiceApplication through the service relationship chain', function () {
$team = Team::factory()->create();
$project = Project::forceCreate([
$project = Project::create([
'uuid' => (string) Str::uuid(),
'name' => 'Test Project',
'team_id' => $team->id,
]);
$environment = Environment::forceCreate([
$environment = Environment::create([
'name' => 'test-env-'.Str::random(8),
'project_id' => $project->id,
]);
$service = Service::forceCreate([
$service = Service::create([
'uuid' => (string) Str::uuid(),
'name' => 'supabase',
'environment_id' => $environment->id,
@@ -67,7 +67,7 @@ it('returns the correct team for ServiceApplication through the service relation
'docker_compose_raw' => 'version: "3"',
]);
$serviceApplication = ServiceApplication::forceCreate([
$serviceApplication = ServiceApplication::create([
'uuid' => (string) Str::uuid(),
'name' => 'supabase-studio',
'service_id' => $service->id,
+1 -1
View File
@@ -49,7 +49,7 @@ function createTestApplication($context): Application
function createTestDatabase($context): StandalonePostgresql
{
return StandalonePostgresql::forceCreate([
return StandalonePostgresql::create([
'name' => 'test-postgres',
'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres',
+10 -8
View File
@@ -1,12 +1,14 @@
<?php
use App\Models\Application;
use App\Models\ApplicationSetting;
/**
* Security tests for git ref validation (GHSA-mw5w-2vvh-mgf4).
*
* Ensures that git_commit_sha and related inputs are validated
* to prevent OS command injection via shell metacharacters.
*/
describe('validateGitRef', function () {
test('accepts valid hex commit SHAs', function () {
expect(validateGitRef('abc123def456'))->toBe('abc123def456');
@@ -93,31 +95,31 @@ describe('validateGitRef', function () {
describe('executeInDocker git log escaping', function () {
test('git log command escapes commit SHA to prevent injection', function () {
$maliciousCommit = "HEAD'; id; #";
$command = "cd /workdir && git log -1 ".escapeshellarg($maliciousCommit).' --pretty=%B';
$command = 'cd /workdir && git log -1 '.escapeshellarg($maliciousCommit).' --pretty=%B';
$result = executeInDocker('test-container', $command);
// The malicious payload must not be able to break out of quoting
expect($result)->not->toContain("id;");
expect($result)->not->toContain('id;');
expect($result)->toContain("'HEAD'\\''");
});
});
describe('buildGitCheckoutCommand escaping', function () {
test('checkout command escapes target to prevent injection', function () {
$app = new \App\Models\Application;
$app->forceFill(['uuid' => 'test-uuid']);
$app = new Application;
$app->fill(['uuid' => 'test-uuid']);
$settings = new \App\Models\ApplicationSetting;
$settings = new ApplicationSetting;
$settings->is_git_submodules_enabled = false;
$app->setRelation('settings', $settings);
$method = new \ReflectionMethod($app, 'buildGitCheckoutCommand');
$method = new ReflectionMethod($app, 'buildGitCheckoutCommand');
$result = $method->invoke($app, 'abc123');
expect($result)->toContain("git checkout 'abc123'");
$result = $method->invoke($app, "abc'; id; #");
expect($result)->not->toContain("id;");
expect($result)->not->toContain('id;');
expect($result)->toContain("git checkout 'abc'");
});
});
@@ -0,0 +1,76 @@
<?php
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview;
use App\Models\ApplicationSetting;
use App\Models\CloudProviderToken;
use App\Models\Environment;
use App\Models\GithubApp;
use App\Models\Project;
use App\Models\ProjectSetting;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledDatabaseBackupExecution;
use App\Models\ScheduledTask;
use App\Models\ScheduledTaskExecution;
use App\Models\Server;
use App\Models\ServerSetting;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDocker;
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\Subscription;
use App\Models\SwarmDocker;
use App\Models\Tag;
use App\Models\User;
it('keeps required mass-assignment attributes fillable for internal create flows', function (string $modelClass, array $expectedAttributes) {
$model = new $modelClass;
expect($model->getFillable())->toContain(...$expectedAttributes);
})->with([
// Relationship/ownership keys
[CloudProviderToken::class, ['team_id']],
[Tag::class, ['team_id']],
[Subscription::class, ['team_id']],
[ScheduledTaskExecution::class, ['scheduled_task_id']],
[ScheduledDatabaseBackupExecution::class, ['uuid', 'scheduled_database_backup_id']],
[ScheduledDatabaseBackup::class, ['uuid', 'team_id']],
[ScheduledTask::class, ['uuid', 'team_id', 'application_id', 'service_id']],
[ServiceDatabase::class, ['service_id']],
[ServiceApplication::class, ['service_id']],
[ApplicationDeploymentQueue::class, ['docker_registry_image_tag']],
[Project::class, ['team_id', 'uuid']],
[Environment::class, ['project_id', 'uuid']],
[ProjectSetting::class, ['project_id']],
[ApplicationSetting::class, ['application_id']],
[ServerSetting::class, ['server_id']],
[SwarmDocker::class, ['server_id']],
[StandaloneDocker::class, ['server_id']],
[User::class, ['pending_email', 'email_change_code', 'email_change_code_expires_at']],
[Server::class, ['ip_previous']],
[GithubApp::class, ['team_id', 'private_key_id']],
// Application/Service resource keys (including uuid for clone flows)
[Application::class, ['uuid', 'environment_id', 'destination_id', 'destination_type', 'source_id', 'source_type', 'repository_project_id', 'private_key_id']],
[ApplicationPreview::class, ['uuid', 'application_id']],
[Service::class, ['uuid', 'environment_id', 'server_id', 'destination_id', 'destination_type']],
// Standalone database resource keys (including uuid for clone flows)
[StandalonePostgresql::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']],
[StandaloneMysql::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']],
[StandaloneMariadb::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']],
[StandaloneMongodb::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']],
[StandaloneRedis::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']],
[StandaloneKeydb::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']],
[StandaloneDragonfly::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']],
[StandaloneClickhouse::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']],
]);
+4 -4
View File
@@ -16,8 +16,8 @@ it('ensures service parser does not include image in trusted service creation qu
expect($parsersFile)
->toContain("\$databaseFound = ServiceDatabase::where('name', \$serviceName)->where('service_id', \$resource->id)->first();")
->toContain("\$applicationFound = ServiceApplication::where('name', \$serviceName)->where('service_id', \$resource->id)->first();")
->toContain("forceCreate([\n 'name' => \$serviceName,\n 'service_id' => \$resource->id,\n ]);")
->not->toContain("forceCreate([\n 'name' => \$serviceName,\n 'image' => \$image,\n 'service_id' => \$resource->id,\n ]);");
->toContain("create([\n 'name' => \$serviceName,\n 'service_id' => \$resource->id,\n ]);")
->not->toContain("create([\n 'name' => \$serviceName,\n 'image' => \$image,\n 'service_id' => \$resource->id,\n ]);");
});
it('ensures service parser updates image after finding or creating service', function () {
@@ -41,8 +41,8 @@ it('ensures parseDockerComposeFile does not create duplicates on null savedServi
// The new code checks for null within the else block and creates only if needed
expect($sharedFile)
->toContain('if (is_null($savedService)) {')
->toContain('$savedService = ServiceDatabase::forceCreate([')
->toContain('$savedService = ServiceApplication::forceCreate([');
->toContain('$savedService = ServiceDatabase::create([')
->toContain('$savedService = ServiceApplication::create([');
});
it('verifies image update logic is present in parseDockerComposeFile', function () {
+3 -3
View File
@@ -77,21 +77,21 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
],
]);
Project::forceCreate([
Project::create([
'uuid' => 'project-1',
'name' => 'My first project',
'description' => 'This is a test project in development',
'team_id' => 0,
]);
Project::forceCreate([
Project::create([
'uuid' => 'project-2',
'name' => 'Production API',
'description' => 'Backend services for production',
'team_id' => 0,
]);
Project::forceCreate([
Project::create([
'uuid' => 'project-3',
'name' => 'Staging Environment',
'description' => 'Staging and QA testing',