Steuerkram/tests/56_NextcloudSsrfTest.php

84 lines
3.6 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.
$this->configure('https://cloud.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://cloud.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);
}
}