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,189 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests;
|
||||
|
||||
use App\Support\UrlValidator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class UrlValidatorTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @return array<string,array{0:string,1:bool}>
|
||||
*/
|
||||
public static function urlProvider(): array
|
||||
{
|
||||
return [
|
||||
// --- akzeptiert ------------------------------------------
|
||||
'https' => ['https://www.beispiel.de/pfad?a=1#anker', true],
|
||||
'http' => ['http://beispiel.de', true],
|
||||
'IP-Adresse' => ['http://192.168.1.10:8080/x', true],
|
||||
'IPv6' => ['https://[2001:db8::1]/x', true],
|
||||
'IPv6 mit Port' => ['https://[2001:db8::1]:8443/x', true],
|
||||
'localhost' => ['http://localhost:8080/test', true],
|
||||
'Umlaut-Domain' => ['https://müller.de', true],
|
||||
'Umlaut-Domain mit Pfad' => ['https://münchen.de/veranstaltung', true],
|
||||
'Punycode-Domain' => ['https://xn--mller-kva.de', true],
|
||||
'Umlaute im Pfad' => ['https://de.wikipedia.org/wiki/München', true],
|
||||
'Umlaute im Query' => ['https://beispiel.de/suche?q=Grüße&x=1', true],
|
||||
'interner Host ohne Punkt' => ['http://intranet/seite', true],
|
||||
'interner Host mit Port' => ['http://nas:5000/freigabe', true],
|
||||
'lange TLD' => ['https://beispiel.photography', true],
|
||||
'Benutzerinfo' => ['https://user:pass@beispiel.de/geschuetzt', true],
|
||||
'absoluter Punkt am Ende' => ['https://beispiel.de./x', true],
|
||||
'Bindestrich in der Domain' => ['https://mein-shop.de', true],
|
||||
|
||||
// --- abgelehnt -------------------------------------------
|
||||
'leer' => ['', false],
|
||||
'ftp' => ['ftp://beispiel.de/datei.zip', false],
|
||||
'javascript' => ['javascript:alert(1)', false],
|
||||
'data-URI' => ['data:text/html,<script>alert(1)</script>', false],
|
||||
'mailto' => ['mailto:info@beispiel.de', false],
|
||||
'file' => ['file:///etc/passwd', false],
|
||||
'ohne Host' => ['https://', false],
|
||||
'Steuerzeichen' => ["https://beispiel.de/\nX", false],
|
||||
'zu lang' => ['https://beispiel.de/' . str_repeat('a', 2100), false],
|
||||
'Bindestrich am Labelanfang' => ['https://-beispiel.de', false],
|
||||
'Bindestrich am Labelende' => ['https://beispiel-.de', false],
|
||||
'leeres Label' => ['https://beispiel..de', false],
|
||||
'ungueltiger Port' => ['https://beispiel.de:99999/x', false],
|
||||
'Unterstrich im Host' => ['https://mein_host.de', false],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider urlProvider
|
||||
*/
|
||||
public function testValidation(string $url, bool $expected): void
|
||||
{
|
||||
self::assertSame($expected, UrlValidator::isValid($url), $url);
|
||||
self::assertSame($expected, UrlValidator::validationError($url) === null);
|
||||
}
|
||||
|
||||
public function testNormalizeAddsHttpsWhenSchemeIsMissing(): void
|
||||
{
|
||||
self::assertSame('https://beispiel.de/pfad', UrlValidator::normalize('beispiel.de/pfad'));
|
||||
self::assertSame('https://www.beispiel.de', UrlValidator::normalize(' www.beispiel.de '));
|
||||
self::assertSame('https://beispiel.de', UrlValidator::normalize('//beispiel.de'));
|
||||
self::assertSame('https://müller.de', UrlValidator::normalize('müller.de'));
|
||||
}
|
||||
|
||||
public function testNormalizeKeepsExistingScheme(): void
|
||||
{
|
||||
self::assertSame('http://beispiel.de', UrlValidator::normalize('http://beispiel.de'));
|
||||
self::assertSame('https://beispiel.de', UrlValidator::normalize('https://beispiel.de'));
|
||||
}
|
||||
|
||||
public function testNormalizeLowercasesSchemeAndHostOnly(): void
|
||||
{
|
||||
self::assertSame(
|
||||
'https://beispiel.de/Pfad/Mit/Grossbuchstaben',
|
||||
UrlValidator::normalize('HTTPS://Beispiel.DE/Pfad/Mit/Grossbuchstaben'),
|
||||
);
|
||||
}
|
||||
|
||||
public function testNormalizeKeepsTheReadableHost(): void
|
||||
{
|
||||
// Punycode gehoert erst in den Location-Header, nicht in die Anzeige.
|
||||
self::assertSame('https://münchen.de/fest', UrlValidator::normalize('https://münchen.de/fest'));
|
||||
}
|
||||
|
||||
public function testNormalizePercentEncodesPathAndQuery(): void
|
||||
{
|
||||
self::assertSame(
|
||||
'https://de.wikipedia.org/wiki/M%C3%BCnchen',
|
||||
UrlValidator::normalize('https://de.wikipedia.org/wiki/München'),
|
||||
);
|
||||
self::assertSame(
|
||||
'https://beispiel.de/suche?q=Gr%C3%BC%C3%9Fe&x=1',
|
||||
UrlValidator::normalize('https://beispiel.de/suche?q=Grüße&x=1'),
|
||||
);
|
||||
self::assertSame(
|
||||
'https://beispiel.de/a%20b',
|
||||
UrlValidator::normalize('https://beispiel.de/a b'),
|
||||
);
|
||||
}
|
||||
|
||||
public function testNormalizeDoesNotDoubleEncode(): void
|
||||
{
|
||||
$alreadyEncoded = 'https://de.wikipedia.org/wiki/M%C3%BCnchen';
|
||||
|
||||
self::assertSame($alreadyEncoded, UrlValidator::normalize($alreadyEncoded));
|
||||
// Zweifaches Normalisieren aendert nichts mehr.
|
||||
self::assertSame(
|
||||
UrlValidator::normalize('https://de.wikipedia.org/wiki/München'),
|
||||
UrlValidator::normalize(UrlValidator::normalize('https://de.wikipedia.org/wiki/München')),
|
||||
);
|
||||
}
|
||||
|
||||
public function testNormalizeKeepsReservedCharactersUsable(): void
|
||||
{
|
||||
$url = 'https://beispiel.de/a/b?x=1&y=2#teil-3';
|
||||
|
||||
self::assertSame($url, UrlValidator::normalize($url));
|
||||
}
|
||||
|
||||
public function testNormalizeDoesNotTurnOtherSchemesIntoHttps(): void
|
||||
{
|
||||
$normalized = UrlValidator::normalize('javascript:alert(1)');
|
||||
|
||||
self::assertSame('javascript:alert(1)', $normalized);
|
||||
self::assertFalse(UrlValidator::isValid($normalized));
|
||||
}
|
||||
|
||||
public function testNormalizedUserInputPassesValidation(): void
|
||||
{
|
||||
foreach (['beispiel.de', 'www.beispiel.de/a/b?c=d', 'müller.de', 'intranet/seite'] as $input) {
|
||||
self::assertTrue(
|
||||
UrlValidator::isValid(UrlValidator::normalize($input)),
|
||||
$input,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testToAsciiUrlConvertsOnlyTheHost(): void
|
||||
{
|
||||
self::assertSame(
|
||||
'https://xn--mller-kva.de/gr%C3%BC%C3%9Fe',
|
||||
UrlValidator::toAsciiUrl('https://müller.de/gr%C3%BC%C3%9Fe'),
|
||||
);
|
||||
self::assertSame(
|
||||
'https://www.xn--mnchen-3ya.de:8080/a?b=1#c',
|
||||
UrlValidator::toAsciiUrl('https://www.münchen.de:8080/a?b=1#c'),
|
||||
);
|
||||
}
|
||||
|
||||
public function testToAsciiUrlLeavesAsciiUrlsAlone(): void
|
||||
{
|
||||
foreach ([
|
||||
'https://beispiel.de/pfad?a=1#b',
|
||||
'http://192.168.1.10:8080/x',
|
||||
'https://[2001:db8::1]/x',
|
||||
'http://intranet/seite',
|
||||
] as $url) {
|
||||
self::assertSame($url, UrlValidator::toAsciiUrl($url));
|
||||
}
|
||||
}
|
||||
|
||||
public function testToAsciiUrlResultIsAlwaysAscii(): void
|
||||
{
|
||||
foreach (['https://müller.de/x', 'https://grüße.example.com/ä', 'https://münchen.de'] as $url) {
|
||||
$ascii = UrlValidator::toAsciiUrl(UrlValidator::normalize($url));
|
||||
|
||||
self::assertSame(
|
||||
1,
|
||||
preg_match('/^[\x20-\x7E]+$/', $ascii),
|
||||
'Location-Header muss ASCII sein: ' . $ascii,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testErrorMessagesAreHelpful(): void
|
||||
{
|
||||
self::assertStringContainsString('http', (string) UrlValidator::validationError('ftp://beispiel.de'));
|
||||
self::assertStringContainsString('http', (string) UrlValidator::validationError('javascript:alert(1)'));
|
||||
self::assertStringContainsString('Ziel-URL', (string) UrlValidator::validationError(''));
|
||||
self::assertStringContainsString('Hostnamen', (string) UrlValidator::validationError('https://'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user