Root cause
Injection & unsafe execution introduced
fix: correct event class names in callEventOnFinish
How AI contributed: Direct introductionPublished Jul 6, 2026 in
coollabsio/coolifyAll 4 findings in this repository
Coolify is an open-source and self-hostable tool for managing servers, applications, and databases. Prior to 4.0.0-beta.474, the buildHelperImage method in app/Livewire/Settings/Index.php constructs a Docker build command using the dev_helper_version field without shell escaping, allowing an attacker who can set the helper version and trigger the helper image build in a development environment to execute arbitrary commands on the server. This issue is fixed in version 4.0.0-beta.474.
buildHelperImage interpolates Livewire dev_helper_version into docker build -t ...:{$version}; parent had the field but no buildHelperImage.
Root cause
fix: correct event class names in callEventOnFinish
Fix
refactor(settings): validate dev_helper_version and escape build args
Fix by Andras Bacsai · no AI marker found
Case facts
@@ -44,6 +44,8 @@ class Index extends Component public bool $forceSaveDomains = false; + public $buildActivityId = null;+ public function render() { return view('livewire.settings.index');@@ -151,4 +153,37 @@ public function submit() return handleError($e, $this); } }++ public function buildHelperImage()+ {+ try {+ if (! isDev()) {+ $this->dispatch('error', 'Building helper image is only available in development mode.');++ return;+ }++ $version = $this->dev_helper_version ?: config('constants.coolify.helper_version');+ if (empty($version)) {+ $this->dispatch('error', 'Please specify a version to build.');++ return;+ }++ $buildCommand = "docker build -t ghcr.io/coollabsio/coolify-helper:{$version} -f docker/coolify-helper/Dockerfile .";++ $activity = remote_process(+ command: [$buildCommand],+ server: $this->server,+ type: 'build-helper-image'+ );++ $this->buildActivityId = $activity->id;+ $this->dispatch('activityMonitor', $activity->id);++ $this->dispatch('success', "Building coolify-helper:{$version}...");+ } catch (\Exception $e) {+ return handleError($e, $this);+ }+ } }@@ -78,10 +78,22 @@ class="px-4 py-2 text-gray-800 cursor-pointer hover:bg-gray-100 dark:hover:bg-co </div> @if(isDev()) <div class="flex gap-2 md:flex-row flex-col w-full">- <x-forms.input id="dev_helper_version" label="Dev Helper Version (Development Only)"- helper="Override the default coolify-helper image version. Leave empty to use the default version from config ({{ config('constants.coolify.helper_version') }}). Examples: 1.0.11, latest, dev"- placeholder="{{ config('constants.coolify.helper_version') }}" />+ <div class="flex-1">+ <x-forms.input id="dev_helper_version" label="Dev Helper Version (Development Only)"+ helper="Override the default coolify-helper image version. Leave empty to use the default version from config ({{ config('constants.coolify.helper_version') }}). Examples: 1.0.11, latest, dev"+ placeholder="{{ config('constants.coolify.helper_version') }}" />+ </div>+ <div class="flex items-end">+ <x-forms.button wire:click="buildHelperImage" type="button">+ Build Image+ </x-forms.button>+ </div> </div>+ @if($buildActivityId)+ <div class="w-full mt-4">+ <livewire:activity-monitor header="Building Helper Image" :activityId="$buildActivityId" :fullHeight="false" />+ </div>+ @endif @endif </div> </div>@@ -394,7 +394,7 @@ public function downloadFromS3() $commands[] = "docker exec {$containerName} mc cp temporary/{$bucket}/{$cleanPath} {$downloadPath}"; // Execute download commands- $activity = remote_process($commands, $this->server, ignore_errors: false, callEventOnFinish: 'App\\Events\\S3DownloadFinished', callEventData: [+ $activity = remote_process($commands, $this->server, ignore_errors: false, callEventOnFinish: 'S3DownloadFinished', callEventData: [ 'downloadPath' => $downloadPath, 'containerName' => $containerName, 'serverId' => $this->server->id,@@ -478,7 +478,7 @@ public function restoreFromS3() $this->importCommands[] = "docker exec {$this->container} sh -c 'echo \"Import finished with exit code $?\"'"; if (! empty($this->importCommands)) {- $activity = remote_process($this->importCommands, $this->server, ignore_errors: true, callEventOnFinish: 'App\\Events\\S3RestoreJobFinished', callEventData: [+ $activity = remote_process($this->importCommands, $this->server, ignore_errors: true, callEventOnFinish: 'S3RestoreJobFinished', callEventData: [ 'scriptPath' => $scriptPath, 'tmpPath' => $tmpPath, 'container' => $this->container,@@ -35,7 +35,7 @@ class Index extends Component #[Validate('required|string|timezone')] public string $instance_timezone; - #[Validate('nullable|string|max:50')]+ #[Validate(['nullable', 'string', 'max:128', 'regex:/^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/'])] public ?string $dev_helper_version = null; public array $domainConflicts = [];@@ -49,6 +49,7 @@ class Index extends Component protected array $messages = [ 'fqdn.url' => 'Invalid instance URL.', 'fqdn.max' => 'URL must not exceed 255 characters.',+ 'dev_helper_version.regex' => 'Dev helper version must match Docker tag format (alphanumeric, _, ., -; first char cannot be . or -).', ]; public function render()@@ -184,14 +185,23 @@ public function buildHelperImage() return; } + $this->validateOnly('dev_helper_version');+ $version = $this->dev_helper_version ?: config('constants.coolify.helper_version'); if (empty($version)) { $this->dispatch('error', 'Please specify a version to build.'); return; } - $buildCommand = "docker build -t ghcr.io/coollabsio/coolify-helper:{$version} -f docker/coolify-helper/Dockerfile .";+ if (! preg_match('/^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/', (string) $version)) {+ $this->dispatch('error', 'Invalid helper version format.');++ return;+ }++ $imageRef = escapeshellarg("ghcr.io/coollabsio/coolify-helper:{$version}");+ $buildCommand = "docker build -t {$imageRef} -f docker/coolify-helper/Dockerfile ."; $activity = remote_process( command: [$buildCommand],@@ -0,0 +1,90 @@+<?php++use App\Livewire\Settings\Index;+use App\Models\InstanceSettings;+use App\Models\Server;+use App\Models\Team;+use App\Models\User;+use Illuminate\Database\Eloquent\Model;+use Illuminate\Foundation\Testing\RefreshDatabase;+use Illuminate\Support\Once;+use Livewire\Livewire;++uses(RefreshDatabase::class);++beforeEach(function () {+ Model::unguarded(function () {+ $this->rootTeam = Team::find(0) ?? Team::create(['id' => 0, 'name' => 'Root Team', 'personal_team' => false]);+ if (! Server::find(0)) {+ Server::factory()->create(['id' => 0, 'team_id' => $this->rootTeam->id]);+ }+ if (! InstanceSettings::find(0)) {+ InstanceSettings::create(['id' => 0]);+ }+ });+ Once::flush();++ $this->user = User::factory()->create();+ $this->rootTeam->members()->attach($this->user->id, ['role' => 'admin']);++ $this->actingAs($this->user);+ session(['currentTeam' => ['id' => $this->rootTeam->id]]);+});++test('dev_helper_version rejects values outside Docker tag grammar on save', function () {+ $invalid = [+ 'latest with spaces',+ 'a$b',+ 'a`b',+ 'a|b',+ 'a;b',+ 'a&b',+ 'a>b',+ 'a<b',+ "a\nb",+ '.bad',+ '-rm',+ ];++ foreach ($invalid as $payload) {+ Livewire::test(Index::class)+ ->set('dev_helper_version', $payload)+ ->call('instantSave')+ ->assertHasErrors(['dev_helper_version']);+ }++ expect(InstanceSettings::find(0)->dev_helper_version)->toBeNull();+});++test('dev_helper_version accepts valid docker tag formats', function () {+ $valid = ['1.0.12', 'latest', 'dev', 'dev-branch_2', 'v1.2.3-rc1', '1_0_0'];++ foreach ($valid as $tag) {+ Livewire::test(Index::class)+ ->set('dev_helper_version', $tag)+ ->call('instantSave')+ ->assertHasNoErrors(['dev_helper_version']);++ expect(InstanceSettings::find(0)->fresh()->dev_helper_version)->toBe($tag);+ }+});++test('buildHelperImage refuses when non-dev environment', function () {+ config(['app.env' => 'production']);++ Livewire::test(Index::class)+ ->set('dev_helper_version', 'latest')+ ->call('buildHelperImage')+ ->assertDispatched('error');+});++test('buildHelperImage refuses previously stored invalid version', function () {+ config(['app.env' => 'local']);++ $settings = InstanceSettings::find(0);+ $settings->forceFill(['dev_helper_version' => 'bad value'])->saveQuietly();++ Livewire::test(Index::class)+ ->call('buildHelperImage')+ ->assertDispatched('error');AI-assisted change 1f598cd834aaadb549e31bdb8eb300b02be4c4b88a8c44f454c7a1c2c51a1879 · Fix c8df55087bd3d3d42b31998448e6ad83c4e85fa346a441183ac0e43c5b23beec