diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php
index 393906b9b..30cae71f1 100644
--- a/app/Actions/Database/StartClickhouse.php
+++ b/app/Actions/Database/StartClickhouse.php
@@ -51,7 +51,7 @@ public function handle(StandaloneClickhouse $database)
],
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
- 'test' => "clickhouse-client --user {$this->database->clickhouse_admin_user} --password {$this->database->clickhouse_admin_password} --query 'SELECT 1'",
+ 'test' => ['CMD', 'clickhouse-client', '--user', (string) $this->database->clickhouse_admin_user, '--password', (string) $this->database->clickhouse_admin_password, '--query', 'SELECT 1'],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php
index cd820523d..addc30be4 100644
--- a/app/Actions/Database/StartDragonfly.php
+++ b/app/Actions/Database/StartDragonfly.php
@@ -107,7 +107,7 @@ public function handle(StandaloneDragonfly $database)
],
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
- 'test' => "redis-cli -a {$this->database->dragonfly_password} ping",
+ 'test' => ['CMD', 'redis-cli', '-a', (string) $this->database->dragonfly_password, 'ping'],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php
index fe80a7d54..e59d6f697 100644
--- a/app/Actions/Database/StartKeydb.php
+++ b/app/Actions/Database/StartKeydb.php
@@ -109,7 +109,7 @@ public function handle(StandaloneKeydb $database)
],
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
- 'test' => "keydb-cli --pass {$this->database->keydb_password} ping",
+ 'test' => ['CMD', 'keydb-cli', '--pass', (string) $this->database->keydb_password, 'ping'],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
@@ -166,7 +166,7 @@ public function handle(StandaloneKeydb $database)
$docker_compose['volumes'] = $volume_names;
}
- if (! is_null($this->database->keydb_conf) || ! empty($this->database->keydb_conf)) {
+ if (! is_null($this->database->keydb_conf) && ! empty($this->database->keydb_conf)) {
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
[
diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php
index 498ba0b0b..ceb1e8b85 100644
--- a/app/Actions/Database/StartMariadb.php
+++ b/app/Actions/Database/StartMariadb.php
@@ -175,7 +175,7 @@ public function handle(StandaloneMariadb $database)
);
}
- if (! is_null($this->database->mariadb_conf) || ! empty($this->database->mariadb_conf)) {
+ if (! is_null($this->database->mariadb_conf) && ! empty($this->database->mariadb_conf)) {
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'],
[
diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php
index 337516405..1b2b338d3 100644
--- a/app/Actions/Database/StartMysql.php
+++ b/app/Actions/Database/StartMysql.php
@@ -175,7 +175,7 @@ public function handle(StandaloneMysql $database)
);
}
- if (! is_null($this->database->mysql_conf) || ! empty($this->database->mysql_conf)) {
+ if (! is_null($this->database->mysql_conf) && ! empty($this->database->mysql_conf)) {
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
[
diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php
index 41e39c811..81cffeb94 100644
--- a/app/Actions/Database/StartPostgresql.php
+++ b/app/Actions/Database/StartPostgresql.php
@@ -111,10 +111,7 @@ public function handle(StandalonePostgresql $database)
],
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
- 'test' => [
- 'CMD-SHELL',
- "psql -U {$this->database->postgres_user} -d {$this->database->postgres_db} -c 'SELECT 1' || exit 1",
- ],
+ 'test' => ['CMD', 'psql', '-U', (string) $this->database->postgres_user, '-d', (string) $this->database->postgres_db, '-c', 'SELECT 1'],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php
index 70df91054..c31b099e4 100644
--- a/app/Actions/Database/StartRedis.php
+++ b/app/Actions/Database/StartRedis.php
@@ -181,7 +181,7 @@ public function handle(StandaloneRedis $database)
);
}
- if (! is_null($this->database->redis_conf) || ! empty($this->database->redis_conf)) {
+ if (! is_null($this->database->redis_conf) && ! empty($this->database->redis_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir.'/redis.conf',
diff --git a/resources/views/livewire/project/database/clickhouse/general.blade.php b/resources/views/livewire/project/database/clickhouse/general.blade.php
index 23286271a..9283172ad 100644
--- a/resources/views/livewire/project/database/clickhouse/general.blade.php
+++ b/resources/views/livewire/project/database/clickhouse/general.blade.php
@@ -54,7 +54,6 @@
readonly value="Starting the database will generate this." canGate="update" :canResource="$database" />
@endif
-
@@ -76,11 +75,12 @@
+
-
+
Advanced
diff --git a/resources/views/livewire/project/database/dragonfly/general.blade.php b/resources/views/livewire/project/database/dragonfly/general.blade.php
index 856fb8d93..ce46e47dd 100644
--- a/resources/views/livewire/project/database/dragonfly/general.blade.php
+++ b/resources/views/livewire/project/database/dragonfly/general.blade.php
@@ -113,14 +113,15 @@
-
-
-
-
-
Advanced
-
+
+
+
+
+
+
Advanced
+
diff --git a/resources/views/livewire/project/database/keydb/general.blade.php b/resources/views/livewire/project/database/keydb/general.blade.php
index 2310242c9..ee3f8fd0c 100644
--- a/resources/views/livewire/project/database/keydb/general.blade.php
+++ b/resources/views/livewire/project/database/keydb/general.blade.php
@@ -113,14 +113,15 @@
-
-
-
+
+
+
+
Advanced
diff --git a/resources/views/livewire/project/database/mariadb/general.blade.php b/resources/views/livewire/project/database/mariadb/general.blade.php
index e3dc39dcf..1154124d1 100644
--- a/resources/views/livewire/project/database/mariadb/general.blade.php
+++ b/resources/views/livewire/project/database/mariadb/general.blade.php
@@ -137,10 +137,12 @@
+
+
diff --git a/resources/views/livewire/project/database/mongodb/general.blade.php b/resources/views/livewire/project/database/mongodb/general.blade.php
index 13a82d350..e9e5d621d 100644
--- a/resources/views/livewire/project/database/mongodb/general.blade.php
+++ b/resources/views/livewire/project/database/mongodb/general.blade.php
@@ -151,10 +151,12 @@
+
+
diff --git a/resources/views/livewire/project/database/mysql/general.blade.php b/resources/views/livewire/project/database/mysql/general.blade.php
index eec9fe1ac..bb3916ec8 100644
--- a/resources/views/livewire/project/database/mysql/general.blade.php
+++ b/resources/views/livewire/project/database/mysql/general.blade.php
@@ -153,10 +153,12 @@
+
+
Advanced
diff --git a/resources/views/livewire/project/database/postgresql/general.blade.php b/resources/views/livewire/project/database/postgresql/general.blade.php
index e8536e735..9c956f5b3 100644
--- a/resources/views/livewire/project/database/postgresql/general.blade.php
+++ b/resources/views/livewire/project/database/postgresql/general.blade.php
@@ -163,10 +163,12 @@
-
-
+
+
+
+
diff --git a/resources/views/livewire/project/database/redis/general.blade.php b/resources/views/livewire/project/database/redis/general.blade.php
index 485c69125..73ee5f0e5 100644
--- a/resources/views/livewire/project/database/redis/general.blade.php
+++ b/resources/views/livewire/project/database/redis/general.blade.php
@@ -132,10 +132,12 @@
-
-
+
+
+
+
["admin\0id"],
+]);
+
+// ─── PostgreSQL ──────────────────────────────────────────────────────────────
+
+test('postgresql healthcheck uses CMD exec-form, not CMD-SHELL', function () {
+ $source = file_get_contents(__DIR__.'/../../app/Actions/Database/StartPostgresql.php');
+
+ expect($source)->not->toContain('CMD-SHELL');
+ expect($source)->toContain("'CMD', 'psql'");
+});
+
+test('postgresql healthcheck exec-form array is injection-safe regardless of input', function (string $malicious) {
+ // Simulate what StartPostgresql now generates
+ $healthcheck = ['CMD', 'psql', '-U', $malicious, '-d', $malicious, '-c', 'SELECT 1'];
+
+ expect($healthcheck[0])->toBe('CMD');
+ expect($healthcheck[0])->not->toBe('CMD-SHELL');
+ // Malicious value is isolated as a single argv element — no shell interprets it
+ expect($healthcheck)->toContain($malicious);
+ expect(is_array($healthcheck))->toBeTrue();
+})->with('malicious_db_inputs');
+
+// ─── KeyDB ────────────────────────────────────────────────────────────────────
+
+test('keydb healthcheck uses CMD exec-form, not a CMD-SHELL string', function () {
+ $source = file_get_contents(__DIR__.'/../../app/Actions/Database/StartKeydb.php');
+
+ expect($source)->not->toContain('CMD-SHELL');
+ expect($source)->toContain("'CMD', 'keydb-cli'");
+});
+
+test('keydb healthcheck exec-form array is injection-safe regardless of input', function (string $malicious) {
+ $healthcheck = ['CMD', 'keydb-cli', '--pass', $malicious, 'ping'];
+
+ expect($healthcheck[0])->toBe('CMD');
+ expect($healthcheck)->toContain($malicious);
+ expect(is_array($healthcheck))->toBeTrue();
+})->with('malicious_db_inputs');
+
+// ─── Dragonfly ────────────────────────────────────────────────────────────────
+
+test('dragonfly healthcheck uses CMD exec-form, not a CMD-SHELL string', function () {
+ $source = file_get_contents(__DIR__.'/../../app/Actions/Database/StartDragonfly.php');
+
+ expect($source)->not->toContain('CMD-SHELL');
+ expect($source)->toContain("'CMD', 'redis-cli'");
+});
+
+test('dragonfly healthcheck exec-form array is injection-safe regardless of input', function (string $malicious) {
+ $healthcheck = ['CMD', 'redis-cli', '-a', $malicious, 'ping'];
+
+ expect($healthcheck[0])->toBe('CMD');
+ expect($healthcheck)->toContain($malicious);
+ expect(is_array($healthcheck))->toBeTrue();
+})->with('malicious_db_inputs');
+
+// ─── ClickHouse ───────────────────────────────────────────────────────────────
+
+test('clickhouse healthcheck uses CMD exec-form, not a CMD-SHELL string', function () {
+ $source = file_get_contents(__DIR__.'/../../app/Actions/Database/StartClickhouse.php');
+
+ expect($source)->not->toContain('CMD-SHELL');
+ expect($source)->toContain("'CMD', 'clickhouse-client'");
+});
+
+test('clickhouse healthcheck exec-form array is injection-safe regardless of input', function (string $malicious) {
+ $healthcheck = ['CMD', 'clickhouse-client', '--user', $malicious, '--password', $malicious, '--query', 'SELECT 1'];
+
+ expect($healthcheck[0])->toBe('CMD');
+ expect($healthcheck)->toContain($malicious);
+ expect(is_array($healthcheck))->toBeTrue();
+})->with('malicious_db_inputs');
+
+// ─── Verify unaffected databases still use their safe patterns ────────────────
+
+test('mysql healthcheck already uses CMD exec-form (no regression)', function () {
+ $source = file_get_contents(__DIR__.'/../../app/Actions/Database/StartMysql.php');
+
+ // MySQL already used CMD array form — ensure it stays that way
+ expect($source)->toContain("'CMD', 'mysqladmin'");
+});
+
+test('mariadb healthcheck uses safe fixed script (no regression)', function () {
+ $source = file_get_contents(__DIR__.'/../../app/Actions/Database/StartMariadb.php');
+
+ expect($source)->toContain('healthcheck.sh');
+ // Must not have gained any user-field interpolation
+ expect($source)->not->toMatch('/CMD-SHELL.*mariadb/i');
+});