72 lines
2.8 KiB
PHP
72 lines
2.8 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use Steuerkram\Services\SsrfGuard;
|
||
|
||
/**
|
||
* Test 74: IP-Pinning funktioniert auch bei Dual-Stack-Hosts (A + AAAA).
|
||
*
|
||
* Regression zum Nextcloud-Import: resolvePinned() lieferte pro IP einen eigenen
|
||
* CURLOPT_RESOLVE-Eintrag. libcurl behandelt mehrere Einträge für dieselbe
|
||
* HOST:PORT-Kombination aber nicht als Alternativen – der zuletzt gesetzte
|
||
* verdrängt die vorherigen. Fiel dabei die IPv6-Adresse an und hatte der
|
||
* Container keine IPv6-Konnektivität, scheiterte jeder Request mit
|
||
* "Could not connect to server". Alle Adressen müssen daher in genau einem
|
||
* Eintrag als Liste stehen.
|
||
*/
|
||
class SsrfPinningDualStackTest extends TestCase
|
||
{
|
||
protected string $testName = 'Test 74: IP-Pinning bei Dual-Stack-Hosts (A + AAAA)';
|
||
|
||
public function run(): void
|
||
{
|
||
$entries = SsrfGuard::resolvePinned('https://example.com/dav');
|
||
$this->assertEquals(1, count($entries), 'resolvePinned() liefert genau einen CURLOPT_RESOLVE-Eintrag');
|
||
|
||
$entry = $entries[0];
|
||
$this->assert(
|
||
str_starts_with($entry, 'example.com:443:'),
|
||
'Eintrag beginnt mit "example.com:443:" (Eintrag: ' . $entry . ')'
|
||
);
|
||
|
||
$addresses = explode(',', substr($entry, strlen('example.com:443:')));
|
||
$this->assertNotEmpty($addresses, 'Der Eintrag enthält mindestens eine Adresse');
|
||
|
||
foreach ($addresses as $address) {
|
||
// IPv6 muss geklammert sein, sonst sind seine Doppelpunkte in der Liste nicht abgrenzbar
|
||
$isBracketed = str_starts_with($address, '[') && str_ends_with($address, ']');
|
||
$ip = $isBracketed ? substr($address, 1, -1) : $address;
|
||
|
||
$this->assert(
|
||
filter_var($ip, FILTER_VALIDATE_IP) !== false,
|
||
'Adresse "' . $address . '" ist eine gültige IP'
|
||
);
|
||
$this->assertEquals(
|
||
str_contains($ip, ':'),
|
||
$isBracketed,
|
||
'Genau IPv6-Adressen stehen in eckigen Klammern (Adresse: ' . $address . ')'
|
||
);
|
||
}
|
||
|
||
// Ein Dual-Stack-Host darf nicht dazu führen, dass eine Adressfamilie verlorengeht:
|
||
// beide aufgelösten Familien müssen im selben Eintrag landen.
|
||
$resolved = [];
|
||
foreach ((array)@dns_get_record('example.com', DNS_A + DNS_AAAA) as $record) {
|
||
if (!empty($record['ip'])) {
|
||
$resolved[] = $record['ip'];
|
||
}
|
||
if (!empty($record['ipv6'])) {
|
||
$resolved[] = $record['ipv6'];
|
||
}
|
||
}
|
||
|
||
foreach ($resolved as $ip) {
|
||
$expected = str_contains($ip, ':') ? '[' . $ip . ']' : $ip;
|
||
$this->assert(
|
||
in_array($expected, $addresses, true),
|
||
'Aufgelöste Adresse ' . $ip . ' ist im Pinning-Eintrag enthalten'
|
||
);
|
||
}
|
||
}
|
||
}
|