test: Testsuite fuer Kernlogik, QR-Codes, Domains und Sicherheit
232 Tests, unter anderem: - Slug-Erzeugung, Kollisionsbehandlung und Validierung - URL-Validierung inklusive IDN, Prozentkodierung und abgelehnter Protokolle; Punycode gegengeprueft gegen idn_to_ascii - QR-Erzeugung in allen Groessen, mit Logo und Level-H-Automatik - Weiterleitung gegen eine echte SQLite-Datenbank samt Domain- Aufloesung ueber den Host-Header - Update einer bestehenden Installation auf mehrere Domains - Angriffsseitig: gefaelschte Proxy-Header, Host-Header-Injektion, SVG mit Skripten, XXE und externen Referenzen
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests;
|
||||
|
||||
use App\Qr\LogoStore;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
|
||||
final class LogoStoreTest extends TestCase
|
||||
{
|
||||
private string $uploadDir;
|
||||
private string $tmpDir;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$suffix = bin2hex(random_bytes(4));
|
||||
$this->uploadDir = sys_get_temp_dir() . '/urlshort-uploads-' . $suffix;
|
||||
$this->tmpDir = sys_get_temp_dir() . '/urlshort-tmp-' . $suffix;
|
||||
mkdir($this->uploadDir, 0775, true);
|
||||
mkdir($this->tmpDir, 0775, true);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
foreach ([$this->uploadDir, $this->tmpDir] as $dir) {
|
||||
foreach (glob($dir . '/*') ?: [] as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
if (is_dir($dir)) {
|
||||
rmdir($dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function store(): LogoStore
|
||||
{
|
||||
return new LogoStore($this->uploadDir, 1024 * 1024);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function upload(string $name, string $contents): array
|
||||
{
|
||||
$path = $this->tmpDir . '/' . bin2hex(random_bytes(4));
|
||||
file_put_contents($path, $contents);
|
||||
|
||||
return [
|
||||
'name' => $name,
|
||||
'tmp_name' => $path,
|
||||
'size' => strlen($contents),
|
||||
'error' => UPLOAD_ERR_OK,
|
||||
];
|
||||
}
|
||||
|
||||
private function validPng(int $width = 64, int $height = 64): string
|
||||
{
|
||||
$image = imagecreatetruecolor($width, $height);
|
||||
self::assertNotFalse($image);
|
||||
imagefilledrectangle($image, 0, 0, $width - 1, $height - 1, (int) imagecolorallocate($image, 11, 79, 158));
|
||||
|
||||
ob_start();
|
||||
imagepng($image);
|
||||
$data = (string) ob_get_clean();
|
||||
imagedestroy($image);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function testValidPngIsStoredWithGeneratedName(): void
|
||||
{
|
||||
$filename = $this->store()->store($this->upload('mein logo.png', $this->validPng()), 7);
|
||||
|
||||
self::assertMatchesRegularExpression('/^logo-7-[a-f0-9]{12}\.png$/', $filename);
|
||||
self::assertFileExists($this->uploadDir . '/' . $filename);
|
||||
}
|
||||
|
||||
public function testCorruptPngIsRejected(): void
|
||||
{
|
||||
// Gueltiger PNG-Header, aber unbrauchbare Bilddaten: getimagesize()
|
||||
// akzeptiert das, GD kann es nicht dekodieren.
|
||||
$corrupt = base64_decode(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAHUlEQVQoz2NkYPjPQApgYhhVMKpgVMGoglEFpCsAAFXsAQEt4A0oAAAAAElFTkSuQmCC',
|
||||
true,
|
||||
);
|
||||
self::assertIsString($corrupt);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessageMatches('/beschaedigt|gelesen/');
|
||||
|
||||
$this->store()->store($this->upload('kaputt.png', $corrupt), 1);
|
||||
}
|
||||
|
||||
public function testOversizedImageIsRejected(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessageMatches('/zu gross/');
|
||||
|
||||
$this->store()->store($this->upload('riesig.png', $this->validPng(4200, 10)), 1);
|
||||
}
|
||||
|
||||
public function testNonImageIsRejected(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessageMatches('/Nur PNG, JPG und SVG/');
|
||||
|
||||
$this->store()->store($this->upload('schad.php', '<?php echo 1;'), 1);
|
||||
}
|
||||
|
||||
public function testTooLargeFileIsRejected(): void
|
||||
{
|
||||
$store = new LogoStore($this->uploadDir, 100);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessageMatches('/hoechstens/');
|
||||
|
||||
$store->store($this->upload('gross.png', $this->validPng()), 1);
|
||||
}
|
||||
|
||||
public function testSvgIsStrippedOfScriptsAndHandlers(): void
|
||||
{
|
||||
$svg = '<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10" onload="alert(1)">'
|
||||
. '<script>alert(2)</script>'
|
||||
. '<circle cx="5" cy="5" r="5" fill="#ff0000" onclick="alert(3)"/>'
|
||||
. '<a href="javascript:alert(4)">x</a>'
|
||||
. '</svg>';
|
||||
|
||||
$filename = $this->store()->store($this->upload('logo.svg', $svg), 3);
|
||||
$stored = (string) file_get_contents($this->uploadDir . '/' . $filename);
|
||||
|
||||
self::assertStringEndsWith('.svg', $filename);
|
||||
self::assertStringNotContainsString('<script', $stored);
|
||||
self::assertStringNotContainsString('onload', $stored);
|
||||
self::assertStringNotContainsString('onclick', $stored);
|
||||
self::assertStringNotContainsString('javascript:', $stored);
|
||||
// Der eigentliche Bildinhalt bleibt erhalten.
|
||||
self::assertStringContainsString('<circle', $stored);
|
||||
self::assertStringContainsString('#ff0000', $stored);
|
||||
}
|
||||
|
||||
public function testSvgWithoutSvgElementIsRejected(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
|
||||
$this->store()->store($this->upload('fake.svg', 'nur text, kein svg'), 1);
|
||||
}
|
||||
|
||||
public function testPathForIgnoresDirectoryTraversal(): void
|
||||
{
|
||||
$store = $this->store();
|
||||
$store->store($this->upload('logo.png', $this->validPng()), 5);
|
||||
|
||||
self::assertNull($store->pathFor('../../../etc/passwd'));
|
||||
self::assertNull($store->pathFor('..\\..\\windows\\win.ini'));
|
||||
self::assertNull($store->pathFor(null));
|
||||
self::assertNull($store->pathFor(''));
|
||||
}
|
||||
|
||||
public function testDeleteRemovesTheFile(): void
|
||||
{
|
||||
$store = $this->store();
|
||||
$filename = $store->store($this->upload('logo.png', $this->validPng()), 9);
|
||||
|
||||
self::assertFileExists($this->uploadDir . '/' . $filename);
|
||||
|
||||
$store->delete($filename);
|
||||
|
||||
self::assertFileDoesNotExist($this->uploadDir . '/' . $filename);
|
||||
}
|
||||
|
||||
public function testIsSvgDetection(): void
|
||||
{
|
||||
$store = $this->store();
|
||||
|
||||
self::assertTrue($store->isSvg('logo-1-abc.svg'));
|
||||
self::assertTrue($store->isSvg('LOGO.SVG'));
|
||||
self::assertFalse($store->isSvg('logo-1-abc.png'));
|
||||
self::assertFalse($store->isSvg(null));
|
||||
}
|
||||
|
||||
public function testMissingUploadIsReported(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessageMatches('/keine Datei/');
|
||||
|
||||
$this->store()->store(['error' => UPLOAD_ERR_NO_FILE], 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user