diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index d718d3735..31e582c9b 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -30,12 +30,14 @@ public function handle(Server $server) ); $caCertPath = config('constants.coolify.base_config_path').'/ssl/'; + $base64Cert = base64_encode($serverCert->ssl_certificate); + $commands = collect([ "mkdir -p $caCertPath", "chown -R 9999:root $caCertPath", "chmod -R 700 $caCertPath", "rm -rf $caCertPath/coolify-ca.crt", - "echo '{$serverCert->ssl_certificate}' > $caCertPath/coolify-ca.crt", + "echo '{$base64Cert}' | base64 -d | tee $caCertPath/coolify-ca.crt > /dev/null", "chmod 644 $caCertPath/coolify-ca.crt", ]); remote_process($commands, $server); diff --git a/app/Livewire/Server/CaCertificate/Show.php b/app/Livewire/Server/CaCertificate/Show.php index c929d9b3d..57aaaa945 100644 --- a/app/Livewire/Server/CaCertificate/Show.php +++ b/app/Livewire/Server/CaCertificate/Show.php @@ -60,10 +60,16 @@ public function saveCaCertificate() throw new \Exception('Certificate content cannot be empty.'); } - if (! openssl_x509_read($this->certificateContent)) { + $parsedCert = openssl_x509_read($this->certificateContent); + if (! $parsedCert) { throw new \Exception('Invalid certificate format.'); } + if (! openssl_x509_export($parsedCert, $cleanedCertificate)) { + throw new \Exception('Failed to process certificate.'); + } + $this->certificateContent = $cleanedCertificate; + if ($this->caCertificate) { $this->caCertificate->ssl_certificate = $this->certificateContent; $this->caCertificate->save(); @@ -114,12 +120,14 @@ private function writeCertificateToServer() { $caCertPath = config('constants.coolify.base_config_path').'/ssl/'; + $base64Cert = base64_encode($this->certificateContent); + $commands = collect([ "mkdir -p $caCertPath", "chown -R 9999:root $caCertPath", "chmod -R 700 $caCertPath", "rm -rf $caCertPath/coolify-ca.crt", - "echo '{$this->certificateContent}' > $caCertPath/coolify-ca.crt", + "echo '{$base64Cert}' | base64 -d | tee $caCertPath/coolify-ca.crt > /dev/null", "chmod 644 $caCertPath/coolify-ca.crt", ]); diff --git a/app/Models/Server.php b/app/Models/Server.php index d693aea6d..49d9c3289 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -1452,12 +1452,14 @@ public function generateCaCertificate() $certificateContent = $caCertificate->ssl_certificate; $caCertPath = config('constants.coolify.base_config_path').'/ssl/'; + $base64Cert = base64_encode($certificateContent); + $commands = collect([ "mkdir -p $caCertPath", "chown -R 9999:root $caCertPath", "chmod -R 700 $caCertPath", "rm -rf $caCertPath/coolify-ca.crt", - "echo '{$certificateContent}' > $caCertPath/coolify-ca.crt", + "echo '{$base64Cert}' | base64 -d | tee $caCertPath/coolify-ca.crt > /dev/null", "chmod 644 $caCertPath/coolify-ca.crt", ]); diff --git a/database/seeders/CaSslCertSeeder.php b/database/seeders/CaSslCertSeeder.php index 1b71a5e43..5d092d2e8 100644 --- a/database/seeders/CaSslCertSeeder.php +++ b/database/seeders/CaSslCertSeeder.php @@ -26,12 +26,14 @@ public function run() } $caCertPath = config('constants.coolify.base_config_path').'/ssl/'; + $base64Cert = base64_encode($caCert->ssl_certificate); + $commands = collect([ "mkdir -p $caCertPath", "chown -R 9999:root $caCertPath", "chmod -R 700 $caCertPath", "rm -rf $caCertPath/coolify-ca.crt", - "echo '{$caCert->ssl_certificate}' > $caCertPath/coolify-ca.crt", + "echo '{$base64Cert}' | base64 -d | tee $caCertPath/coolify-ca.crt > /dev/null", "chmod 644 $caCertPath/coolify-ca.crt", ]); diff --git a/tests/Feature/CaCertificateCommandInjectionTest.php b/tests/Feature/CaCertificateCommandInjectionTest.php new file mode 100644 index 000000000..fffa28d6a --- /dev/null +++ b/tests/Feature/CaCertificateCommandInjectionTest.php @@ -0,0 +1,93 @@ +user = User::factory()->create(); + $this->team = Team::factory()->create(); + $this->user->teams()->attach($this->team, ['role' => 'owner']); + $this->actingAs($this->user); + session(['currentTeam' => $this->team]); + + $this->server = Server::factory()->create([ + 'team_id' => $this->team->id, + ]); +}); + +function generateSelfSignedCert(): string +{ + $key = openssl_pkey_new(['private_key_bits' => 2048]); + $csr = openssl_csr_new(['CN' => 'Test CA'], $key); + $cert = openssl_csr_sign($csr, null, $key, 365); + openssl_x509_export($cert, $certPem); + + return $certPem; +} + +test('saveCaCertificate sanitizes injected commands after certificate marker', function () { + $validCert = generateSelfSignedCert(); + + $caCert = SslCertificate::create([ + 'server_id' => $this->server->id, + 'is_ca_certificate' => true, + 'ssl_certificate' => $validCert, + 'ssl_private_key' => 'test-key', + 'common_name' => 'Coolify CA Certificate', + 'valid_until' => now()->addYears(10), + ]); + + // Inject shell command after valid certificate + $maliciousContent = $validCert."' ; id > /tmp/pwned ; echo '"; + + Livewire::test(Show::class, ['server_uuid' => $this->server->uuid]) + ->set('certificateContent', $maliciousContent) + ->call('saveCaCertificate') + ->assertDispatched('success'); + + // After save, the certificate should be the clean re-exported PEM, not the malicious input + $caCert->refresh(); + expect($caCert->ssl_certificate)->not->toContain('/tmp/pwned'); + expect($caCert->ssl_certificate)->not->toContain('; id'); + expect($caCert->ssl_certificate)->toContain('-----BEGIN CERTIFICATE-----'); + expect($caCert->ssl_certificate)->toEndWith("-----END CERTIFICATE-----\n"); +}); + +test('saveCaCertificate rejects completely invalid certificate', function () { + SslCertificate::create([ + 'server_id' => $this->server->id, + 'is_ca_certificate' => true, + 'ssl_certificate' => 'placeholder', + 'ssl_private_key' => 'test-key', + 'common_name' => 'Coolify CA Certificate', + 'valid_until' => now()->addYears(10), + ]); + + Livewire::test(Show::class, ['server_uuid' => $this->server->uuid]) + ->set('certificateContent', "not-a-cert'; rm -rf /; echo '") + ->call('saveCaCertificate') + ->assertDispatched('error'); +}); + +test('saveCaCertificate rejects empty certificate content', function () { + SslCertificate::create([ + 'server_id' => $this->server->id, + 'is_ca_certificate' => true, + 'ssl_certificate' => 'placeholder', + 'ssl_private_key' => 'test-key', + 'common_name' => 'Coolify CA Certificate', + 'valid_until' => now()->addYears(10), + ]); + + Livewire::test(Show::class, ['server_uuid' => $this->server->uuid]) + ->set('certificateContent', '') + ->call('saveCaCertificate') + ->assertDispatched('error'); +});