71 lines
3 KiB
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);
|
|
}
|
|
}
|