Steuerkram/tests/56_NextcloudSsrfTest.php

87 lines
3.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
use Steuerkram\Database;
use Steuerkram\Services\NextcloudImportService;
/**
* Test 56: SSRF-Schutz für nextcloud_url (Issue #127).
* Prüft, dass NextcloudImportService die konfigurierte Nextcloud-URL beim
* Konstruieren defensiv über SsrfGuard prüft und bei unzulässigen Zielen
* (http://, private/loopback/link-local IPs) keine Verbindung aufbaut,
* statt einen echten Netzwerk-Request auszulösen.
*/
class NextcloudSsrfTest extends TestCase
{
protected string $testName = 'Test 56: SSRF-Schutz für nextcloud_url beim Nextcloud-Belegimport';
public function run(): void
{
$this->assertRejected('http://cloud.example.com', 'http:// wird abgelehnt');
$this->assertRejected('https://127.0.0.1', 'Loopback-IP (127.0.0.1) wird abgelehnt');
$this->assertRejected('https://169.254.169.254', 'Link-Local/Metadata-IP (169.254.169.254) wird abgelehnt');
$this->assertRejected('https://10.0.0.5', 'RFC1918-IP (10.0.0.5) wird abgelehnt');
$this->assertRejected('https://localhost', 'localhost wird abgelehnt');
// Gültiges Ziel: baseUrl bleibt gesetzt, kein configError, davBase korrekt gebildet.
// Hinweis: SsrfGuard löst den Hostnamen per echtem DNS auf (Schutz vor DNS-Rebinding) –
// "cloud.example.com" ist keine registrierte Subdomain und liefert NXDOMAIN. Die
// Apex-Domain "example.com" ist dagegen laut IANA dauerhaft real auflösbar.
$this->configure('https://example.com', 'importuser', 'geheim');
$svc = new NextcloudImportService();
$baseUrl = $this->readProperty($svc, 'baseUrl');
$davBase = $this->readProperty($svc, 'davBase');
$cfgError = $this->readProperty($svc, 'configError');
$this->assertEquals('https://example.com', $baseUrl, 'Gültige https-URL bleibt als baseUrl erhalten');
$this->assert($cfgError === null, 'Kein configError bei gültiger URL');
$this->assert(
str_contains((string)$davBase, '/remote.php/dav/files/importuser'),
'davBase wird aus der gültigen baseUrl gebildet'
);
// Ungültiges Ziel: baseUrl wird geleert, configError gesetzt, import() bricht
// sofort ab (kein PROPFIND-Versuch gegen das interne Ziel).
$this->configure('https://127.0.0.1:9999', 'importuser', 'geheim');
$svc = new NextcloudImportService();
$baseUrl = $this->readProperty($svc, 'baseUrl');
$cfgError = $this->readProperty($svc, 'configError');
$this->assertEquals('', $baseUrl, 'Ungültige URL: baseUrl wird geleert (fail closed)');
$this->assert(
is_string($cfgError) && str_contains($cfgError, 'ungültig'),
'Ungültige URL: configError beschreibt die Ablehnung'
);
$result = $svc->import();
$this->assertEquals(1, $result['errors'], 'import() meldet einen Fehler statt einen Request abzusetzen');
$this->assert(
!empty($result['details']) && str_contains($result['details'][0], 'ungültig'),
'import()-Fehlermeldung verweist auf die ungültige Nextcloud-URL'
);
}
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);
}
private function assertRejected(string $url, string $message): void
{
$this->configure($url, 'importuser', 'geheim');
$svc = new NextcloudImportService();
$baseUrl = $this->readProperty($svc, 'baseUrl');
$this->assert($baseUrl === '', $message);
}
}