target = sys_get_temp_dir().'/kant-zip-'.bin2hex(random_bytes(6)); mkdir($this->target, 0o775, true); } protected function tearDown(): void { foreach (glob($this->target.'/*') ?: [] as $entry) { is_dir($entry) ? self::removeDirectory($entry) : @unlink($entry); } @rmdir($this->target); } public function testTheVersionFolderIsStrippedAndTheTargetIsRespected(): void { $target = str_replace('\\', '/', $this->target); self::assertSame($target.'/public/index.php', kant_target_path('kantsites-1.2.0/public/index.php', $this->target)); // Without the version folder the entry is taken as it is. self::assertSame($target.'/bin/console', kant_target_path('bin/console', $this->target)); } /** * @param string $entry the archive entry a manipulated archive could contain */ #[\PHPUnit\Framework\Attributes\DataProvider('dangerousEntries')] public function testNothingLandsOutsideTheTargetDirectory(string $entry): void { self::assertNull(kant_target_path($entry, $this->target), $entry.' darf nirgends landen.'); } public static function dangerousEntries(): iterable { yield 'zwei Ebenen nach oben' => ['kantsites-1.2.0/../../.env']; yield 'direkt nach oben' => ['../evil.php']; yield 'mitten im Pfad' => ['kantsites-1.2.0/src/../../../evil.php']; yield 'absolut' => ['/etc/passwd']; yield 'Windows-Laufwerk' => ['C:/Windows/evil.php']; yield 'Rückwärtsschrägstrich' => ['kantsites-1.2.0\\..\\..\\evil.php']; yield 'Nullbyte' => ["kantsites-1.2.0/src/evil\0.php"]; yield 'nur das Verzeichnis' => ['kantsites-1.2.0/']; yield 'leer' => ['']; } public function testAnOrdinaryArchiveIsUnpackedAndAManipulatedOneIsNot(): void { $good = $this->archive(['kantsites-1.2.0/public/index.php' => ' 'x']); self::assertSame(2, kant_unpack($good, $this->target)); self::assertSame('target.'/public/index.php')); self::assertFileExists($this->target.'/src/Kernel.php'); // An archive with an entry that points outside is refused - and the refusal happens while // unpacking, so what matters is that the file outside never appears. $evil = $this->archive(['kantsites-1.2.0/harmlos.txt' => 'ok', 'kantsites-1.2.0/../entwischt.txt' => 'böse']); try { kant_unpack($evil, $this->target); self::fail('Ein Eintrag außerhalb des Ziels muss zum Abbruch führen.'); } catch (\RuntimeException $e) { self::assertStringContainsString('außerhalb', $e->getMessage()); } self::assertFileDoesNotExist(\dirname($this->target).'/entwischt.txt'); } public function testThePreflightNamesWhatIsMissing(): void { $checks = kant_checks($this->target); $labels = array_column($checks, 'label'); self::assertContains('ZIP-Erweiterung', $labels); self::assertContains('Schreibrecht im Zielverzeichnis', $labels); // This copy comes from the repository, not from a release: no address, no checksum, and // it says so instead of downloading something. $unfinished = self::find($checks, 'Adresse und Prüfsumme eingetragen'); self::assertFalse($unfinished['ok']); // An existing installation is not overwritten, not even partially. mkdir($this->target.'/public', 0o775, true); file_put_contents($this->target.'/public/index.php', 'x'); self::assertFalse(self::find(kant_checks($this->target), 'Verzeichnis ist leer genug')['ok']); } private function archive(array $entries): string { $path = $this->target.'/../kant-archive-'.bin2hex(random_bytes(4)).'.zip'; $zip = new \ZipArchive(); $zip->open($path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE); foreach ($entries as $name => $content) { $zip->addFromString($name, $content); } $zip->close(); self::assertFileExists($path); register_shutdown_function(static fn () => @unlink($path)); return $path; } private static function find(array $checks, string $label): array { foreach ($checks as $check) { if ($check['label'] === $label) { return $check; } } self::fail('Prüfung fehlt: '.$label); } private static function removeDirectory(string $path): void { foreach (scandir($path) ?: [] as $entry) { if ('.' !== $entry && '..' !== $entry) { is_dir($path.'/'.$entry) ? self::removeDirectory($path.'/'.$entry) : @unlink($path.'/'.$entry); } } @rmdir($path); } }