diff --git a/bootstrap/helpers/sudo.php b/bootstrap/helpers/sudo.php index acbe45c1c9..8d662485f0 100644 --- a/bootstrap/helpers/sudo.php +++ b/bootstrap/helpers/sudo.php @@ -27,7 +27,7 @@ function shouldChangeOwnership(string $path): bool */ function ownershipCommand(string $path, Server $server): string { - return "find $path -user root -exec chown $server->user:$server->user {} + && chmod o-rwx $path"; + return "find $path -user root -exec chown -h $server->user:$server->user {} + && chmod o-rwx $path"; } /** diff --git a/tests/Unit/ParseCommandsByLineForSudoTest.php b/tests/Unit/ParseCommandsByLineForSudoTest.php index 5a98398da2..594ecbd046 100644 --- a/tests/Unit/ParseCommandsByLineForSudoTest.php +++ b/tests/Unit/ParseCommandsByLineForSudoTest.php @@ -276,7 +276,7 @@ test('adds ownership changes for Coolify data paths', function () { $result = parseCommandsByLineForSudo($commands, $this->server); - expect($result[0])->toBe('sudo mkdir -p /data/coolify/logs && sudo find /data/coolify/logs -user root -exec chown ubuntu:ubuntu {} + && sudo chmod o-rwx /data/coolify/logs'); + expect($result[0])->toBe('sudo mkdir -p /data/coolify/logs && sudo find /data/coolify/logs -user root -exec chown -h ubuntu:ubuntu {} + && sudo chmod o-rwx /data/coolify/logs'); }); test('adds ownership changes for Coolify tmp paths', function () { @@ -286,7 +286,7 @@ test('adds ownership changes for Coolify tmp paths', function () { $result = parseCommandsByLineForSudo($commands, $this->server); - expect($result[0])->toBe('sudo mkdir -p /tmp/coolify/cache && sudo find /tmp/coolify/cache -user root -exec chown ubuntu:ubuntu {} + && sudo chmod o-rwx /tmp/coolify/cache'); + expect($result[0])->toBe('sudo mkdir -p /tmp/coolify/cache && sudo find /tmp/coolify/cache -user root -exec chown -h ubuntu:ubuntu {} + && sudo chmod o-rwx /tmp/coolify/cache'); }); test('ownership changes work where root may not use sudo', function (string $parser) { @@ -372,7 +372,7 @@ test('ownership changes apply only to the mkdir path when more commands follow', $process = new Process(['bash', '-c', $command], env: ['PATH' => "{$directory}:".getenv('PATH'), 'LOG' => "{$directory}/log"]); $process->run(); - expect($command)->toContain("find {$path}/dynamic -user root -exec chown ubuntu:ubuntu {} + && ") + expect($command)->toContain("find {$path}/dynamic -user root -exec chown -h ubuntu:ubuntu {} + && ") ->and($command)->toContain("chmod o-rwx {$path}/dynamic{$separator} ") ->and($command)->toEndWith('head -n 1000'.($parser === 'command list' ? "'" : '')) ->and($process->getErrorOutput())->toBe('') diff --git a/tests/Unit/SudoOwnershipSymlinkTest.php b/tests/Unit/SudoOwnershipSymlinkTest.php new file mode 100644 index 0000000000..b9ead5ff57 --- /dev/null +++ b/tests/Unit/SudoOwnershipSymlinkTest.php @@ -0,0 +1,50 @@ +markTestSkipped('Needs root and a daemon user to change file owners.'); + } + if ($tools === 'busybox' && ! is_executable('/usr/bin/busybox')) { + $this->markTestSkipped('Needs BusyBox.'); + } + $directory = sys_get_temp_dir().'/coolify-sudo-'.bin2hex(random_bytes(4)); + mkdir($directory); + file_put_contents("{$directory}/sudo", "#!/bin/sh\nexec \"\$@\"\n"); + chmod("{$directory}/sudo", 0755); + if ($tools === 'busybox') { + foreach (['find', 'chown', 'chmod', 'mkdir'] as $tool) { + symlink('/usr/bin/busybox', "{$directory}/{$tool}"); + } + } + // A root-owned file outside Coolify's directories, like /etc/sudoers. + $target = "{$directory}/sudoers"; + file_put_contents($target, 'root ALL=(ALL) ALL'); + $path = '/tmp/coolify/symlink-test-'.bin2hex(random_bytes(4)); + mkdir($path, 0700, true); + symlink($target, "{$path}/link"); + + $server = Mockery::mock(Server::class)->makePartial(); + $server->shouldReceive('getAttribute')->with('user')->andReturn('daemon'); + $server->shouldReceive('setAttribute')->andReturnSelf(); + $command = $parser === 'command list' + ? parseCommandsByLineForSudo(collect(["mkdir -p {$path}"]), $server)[0] + : parseLineForSudo("mkdir -p {$path}", $server); + $process = new Process(['/bin/sh', '-c', $command], env: ['PATH' => "{$directory}:".getenv('PATH')]); + $process->run(); + clearstatcache(); + + expect($process->getErrorOutput())->toBe('') + ->and($process->isSuccessful())->toBeTrue() + ->and(fileowner($target))->toBe(0) + ->and(lstat("{$path}/link")['uid'])->toBe(1) + ->and(fileowner($path))->toBe(1); + + (new Process(['rm', '-rf', $directory, $path]))->run(); +})->with(['command list', 'deployment line'])->with(['coreutils', 'busybox']);