fix(ca-cert): prevent command injection via base64 encoding (#8617)
This commit is contained in:
commit
3eb9426b95
5 changed files with 112 additions and 5 deletions
|
|
@ -30,12 +30,14 @@ public function handle(Server $server)
|
||||||
);
|
);
|
||||||
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||||
|
|
||||||
|
$base64Cert = base64_encode($serverCert->ssl_certificate);
|
||||||
|
|
||||||
$commands = collect([
|
$commands = collect([
|
||||||
"mkdir -p $caCertPath",
|
"mkdir -p $caCertPath",
|
||||||
"chown -R 9999:root $caCertPath",
|
"chown -R 9999:root $caCertPath",
|
||||||
"chmod -R 700 $caCertPath",
|
"chmod -R 700 $caCertPath",
|
||||||
"rm -rf $caCertPath/coolify-ca.crt",
|
"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",
|
"chmod 644 $caCertPath/coolify-ca.crt",
|
||||||
]);
|
]);
|
||||||
remote_process($commands, $server);
|
remote_process($commands, $server);
|
||||||
|
|
|
||||||
|
|
@ -60,10 +60,16 @@ public function saveCaCertificate()
|
||||||
throw new \Exception('Certificate content cannot be empty.');
|
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.');
|
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) {
|
if ($this->caCertificate) {
|
||||||
$this->caCertificate->ssl_certificate = $this->certificateContent;
|
$this->caCertificate->ssl_certificate = $this->certificateContent;
|
||||||
$this->caCertificate->save();
|
$this->caCertificate->save();
|
||||||
|
|
@ -114,12 +120,14 @@ private function writeCertificateToServer()
|
||||||
{
|
{
|
||||||
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||||
|
|
||||||
|
$base64Cert = base64_encode($this->certificateContent);
|
||||||
|
|
||||||
$commands = collect([
|
$commands = collect([
|
||||||
"mkdir -p $caCertPath",
|
"mkdir -p $caCertPath",
|
||||||
"chown -R 9999:root $caCertPath",
|
"chown -R 9999:root $caCertPath",
|
||||||
"chmod -R 700 $caCertPath",
|
"chmod -R 700 $caCertPath",
|
||||||
"rm -rf $caCertPath/coolify-ca.crt",
|
"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",
|
"chmod 644 $caCertPath/coolify-ca.crt",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1452,12 +1452,14 @@ public function generateCaCertificate()
|
||||||
$certificateContent = $caCertificate->ssl_certificate;
|
$certificateContent = $caCertificate->ssl_certificate;
|
||||||
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||||
|
|
||||||
|
$base64Cert = base64_encode($certificateContent);
|
||||||
|
|
||||||
$commands = collect([
|
$commands = collect([
|
||||||
"mkdir -p $caCertPath",
|
"mkdir -p $caCertPath",
|
||||||
"chown -R 9999:root $caCertPath",
|
"chown -R 9999:root $caCertPath",
|
||||||
"chmod -R 700 $caCertPath",
|
"chmod -R 700 $caCertPath",
|
||||||
"rm -rf $caCertPath/coolify-ca.crt",
|
"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",
|
"chmod 644 $caCertPath/coolify-ca.crt",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,14 @@ public function run()
|
||||||
}
|
}
|
||||||
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||||
|
|
||||||
|
$base64Cert = base64_encode($caCert->ssl_certificate);
|
||||||
|
|
||||||
$commands = collect([
|
$commands = collect([
|
||||||
"mkdir -p $caCertPath",
|
"mkdir -p $caCertPath",
|
||||||
"chown -R 9999:root $caCertPath",
|
"chown -R 9999:root $caCertPath",
|
||||||
"chmod -R 700 $caCertPath",
|
"chmod -R 700 $caCertPath",
|
||||||
"rm -rf $caCertPath/coolify-ca.crt",
|
"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",
|
"chmod 644 $caCertPath/coolify-ca.crt",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
93
tests/Feature/CaCertificateCommandInjectionTest.php
Normal file
93
tests/Feature/CaCertificateCommandInjectionTest.php
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Livewire\Server\CaCertificate\Show;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
|
use App\Models\Team;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->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');
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue