Steuerkram/tests/63_SsrfPinningTest.php

71 lines
3 KiB
PHP

<?php
declare(strict_types=1);
use Steuerkram\Database;
use Steuerkram\Services\NextcloudImportService;
use Steuerkram\Services\SsrfGuard;
/**
* Test 63: SSRF-Schutz gegen DNS-Rebinding/TOCTOU (Issue #130).
* SsrfGuard::resolvePinned() liefert für gültige https-Hosts CURLOPT_RESOLVE-Einträge
* mit den bereits geprüften IP-Adressen zurück, statt dass der spätere Request den
* Hostnamen erneut selbst auflösen muss. NextcloudImportService übernimmt diese
* Einträge und nutzt sie für alle eigenen cURL-Requests.
*/
class SsrfPinningTest extends TestCase
{
protected string $testName = 'Test 63: SSRF-Schutz gegen DNS-Rebinding (IP-Pinning)';
public function run(): void
{
// Hostname: resolvePinned() liefert nicht-leere CURLOPT_RESOLVE-Einträge im
// Format "HOST:PORT:IP" mit dem tatsächlich geprüften Hostnamen und Port.
$entries = SsrfGuard::resolvePinned('https://example.com/fints');
$this->assertNotEmpty($entries, 'resolvePinned() liefert Einträge für einen gültigen Hostnamen');
foreach ($entries as $entry) {
$this->assert(
str_starts_with($entry, 'example.com:443:'),
'CURLOPT_RESOLVE-Eintrag beginnt mit "example.com:443:" (Eintrag: ' . $entry . ')'
);
}
// IP-Literal als Host: kein Pinning nötig/möglich, resolvePinned() liefert leeres Array,
// validiert aber weiterhin gegen private/interne Bereiche.
$entries = SsrfGuard::resolvePinned('https://8.8.8.8/fints');
$this->assertEquals([], $entries, 'resolvePinned() liefert kein Pinning für ein bereits öffentliches IP-Literal');
try {
SsrfGuard::resolvePinned('https://127.0.0.1/fints');
$this->assert(false, 'resolvePinned() lehnt private IP-Literale weiterhin ab');
} catch (\RuntimeException $e) {
$this->assert(true, 'resolvePinned() lehnt private IP-Literale weiterhin ab');
}
// NextcloudImportService: pinnedResolve wird beim Konstruieren aus der geprüften
// baseUrl befüllt und für alle eigenen cURL-Requests verwendet (newCurl()).
$this->configure('https://example.com', 'importuser', 'geheim');
$svc = new NextcloudImportService();
$pinned = $this->readProperty($svc, 'pinnedResolve');
$this->assertNotEmpty($pinned, 'NextcloudImportService pinnt die IP(s) der geprüften baseUrl');
$this->assert(
str_starts_with($pinned[0], 'example.com:443:'),
'Gepinnter Eintrag bezieht sich auf den Host der baseUrl'
);
}
private function configure(string $url, string $user, string $pass): void
{
Database::execute(
'UPDATE company_settings SET nextcloud_url=?, nextcloud_user=?, nextcloud_pass=? WHERE id=1',
[$url, $user, $pass]
);
}
private function readProperty(object $obj, string $name): mixed
{
$prop = new \ReflectionProperty($obj, $name);
$prop->setAccessible(true);
return $prop->getValue($obj);
}
}