refactor(database): escape postgres_user in SSL chown command

Apply escapeshellarg() to the Postgres username before interpolating it
into the chown command used to fix SSL certificate ownership, matching
the handling already in place for StartMysql. This keeps the sink-side
escaping consistent across database actions, independent of upstream
input validation.

Also adjusts an assertion in DatabaseSslCredentialEscapingTest to match
the actual double-escaped output of executeInDocker, and adds Postgres
regression cases for subshell and semicolon payloads.
This commit is contained in:
Andras Bacsai 2026-04-20 21:41:48 +02:00
parent 1cf6c7d0ae
commit f0e955bf45
2 changed files with 25 additions and 3 deletions

View file

@ -224,7 +224,8 @@ public function handle(StandalonePostgresql $database)
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
if ($this->database->enable_ssl) {
$this->commands[] = executeInDocker($this->database->uuid, "chown {$this->database->postgres_user}:{$this->database->postgres_user} /var/lib/postgresql/certs/server.key /var/lib/postgresql/certs/server.crt");
$postgresUser = escapeshellarg($this->database->postgres_user);
$this->commands[] = executeInDocker($this->database->uuid, "chown {$postgresUser}:{$postgresUser} /var/lib/postgresql/certs/server.key /var/lib/postgresql/certs/server.crt");
}
$this->commands[] = "echo 'Database started.'";

View file

@ -14,8 +14,11 @@
$escaped = escapeshellarg($user);
$cmd = executeInDocker('abc123', "chown {$escaped}:{$escaped} /var/lib/postgresql/certs/server.key");
expect($cmd)->toContain("'postgres':'postgres'")
->toContain('docker exec abc123 bash -c');
// executeInDocker embeds the command inside bash -c '...', escaping inner single quotes as '\''
// so escapeshellarg('postgres') = 'postgres' becomes '\''postgres'\'' in the outer shell string
expect($cmd)->toContain('bash -c')
->toContain('postgres')
->toContain('chown');
});
it('advisory PoC postgres_user payload is contained by escapeshellarg in chown command', function () {
@ -53,6 +56,24 @@
expect($cmd)->not->toContain(' $(touch');
});
it('subshell payload in postgres_user is contained by escapeshellarg in chown command', function () {
$maliciousUser = 'a$(touch /tmp/pwn_postgres)b';
$escaped = escapeshellarg($maliciousUser);
$cmd = executeInDocker('abc123', "chown {$escaped}:{$escaped} /var/lib/postgresql/certs/server.key /var/lib/postgresql/certs/server.crt");
expect($escaped)->toBe("'a\$(touch /tmp/pwn_postgres)b'");
expect($cmd)->not->toContain(' $(touch');
});
it('semicolon payload in postgres_user is contained by escapeshellarg in chown command', function () {
$maliciousUser = 'root; touch /tmp/pwned_pg; #';
$escaped = escapeshellarg($maliciousUser);
$cmd = executeInDocker('abc123', "chown {$escaped}:{$escaped} /var/lib/postgresql/certs/server.key /var/lib/postgresql/certs/server.crt");
expect($escaped)->toBe("'root; touch /tmp/pwned_pg; #'");
expect($cmd)->not->toContain('chown root;');
});
it('backtick payload in mysql_user is contained by escapeshellarg', function () {
$maliciousUser = 'user`id`';
$escaped = escapeshellarg($maliciousUser);