diff --git a/app/Services/ContainerFilesystemService.php b/app/Services/ContainerFilesystemService.php index 32a2750bdd..7a896fc42a 100644 --- a/app/Services/ContainerFilesystemService.php +++ b/app/Services/ContainerFilesystemService.php @@ -85,6 +85,56 @@ class ContainerFilesystemService return (string) base64_decode(trim($encoded), true); } + public function buildWriteCommand(string $path, string $content): string + { + $escaped = $this->escapePath($path, 'write path'); + $b64 = base64_encode($content); + + return $this->dockerExecShell("echo {$b64} | base64 -d > {$escaped}"); + } + + public function buildMkdirCommand(string $path): string + { + $escaped = $this->escapePath($path, 'mkdir path'); + + return $this->dockerExecShell("mkdir -p -- {$escaped}"); + } + + public function buildRenameCommand(string $from, string $to): string + { + $escapedFrom = $this->escapePath($from, 'rename source'); + $escapedTo = $this->escapePath($to, 'rename target'); + + return $this->dockerExecShell("mv -- {$escapedFrom} {$escapedTo}"); + } + + public function buildDeleteCommand(string $path): string + { + $escaped = $this->escapePath($path, 'delete path'); + + return $this->dockerExecShell("rm -rf -- {$escaped}"); + } + + public function write(string $path, string $content): void + { + instant_remote_process([$this->buildWriteCommand($path, $content)], $this->server); + } + + public function makeDirectory(string $path): void + { + instant_remote_process([$this->buildMkdirCommand($path)], $this->server); + } + + public function rename(string $from, string $to): void + { + instant_remote_process([$this->buildRenameCommand($from, $to)], $this->server); + } + + public function delete(string $path): void + { + instant_remote_process([$this->buildDeleteCommand($path)], $this->server); + } + public function defaultRoot(): string { $escapedContainer = escapeshellarg($this->container); diff --git a/tests/Feature/ContainerFilesystemServiceTest.php b/tests/Feature/ContainerFilesystemServiceTest.php index 4c4f6dd112..d2617cb3b1 100644 --- a/tests/Feature/ContainerFilesystemServiceTest.php +++ b/tests/Feature/ContainerFilesystemServiceTest.php @@ -123,3 +123,31 @@ it('reads an editable text file', function () { expect($content)->toBe("hello world\n"); }); + +it('base64-encodes content in the write command', function () { + $cmd = fsService()->buildWriteCommand('/app/a.txt', "hi\n"); + + expect($cmd) + ->toContain('base64 -d') + ->toContain(base64_encode("hi\n")) + ->toContain(escapeshellarg('/app/a.txt')); +}); + +it('builds mkdir, rename and delete commands with escaped paths', function () { + $svc = fsService(); + + expect($svc->buildMkdirCommand('/app/new dir')) + ->toContain('mkdir -p') + ->toContain(escapeshellarg('/app/new dir')); + expect($svc->buildRenameCommand('/app/a', '/app/b')) + ->toContain('mv') + ->toContain(escapeshellarg('/app/a')) + ->toContain(escapeshellarg('/app/b')); + expect($svc->buildDeleteCommand('/app/x')) + ->toContain('rm -rf') + ->toContain(escapeshellarg('/app/x')); +}); + +it('rejects unsafe paths in mutating builders', function () { + fsService()->buildDeleteCommand('/app/$(reboot)'); +})->throws(Exception::class);