87 lines
3.9 KiB
PHP
87 lines
3.9 KiB
PHP
<?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);
|
||
}
|
||
}
|