Korjaa IPv6 IP-allow: IPv4-mapped IPv6 ↔ IPv4 cross-matching

Kun palvelin käyttää IPv6:ta, client IP voi tulla muodossa
::ffff:192.168.1.1 vaikka allow-listassa on 192.168.1.1.
Nyt isIpAllowed() tunnistaa IPv4-mapped IPv6 -osoitteet ja
vertailee molemmissa muodoissa (IPv4 ↔ ::ffff:IPv4).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 18:02:16 +02:00
parent 9208ab387a
commit 6628eaeb89

41
api.php
View File

@@ -146,8 +146,22 @@ function isIpAllowed(string $ip, string $allowedIps): bool {
$allowedIps = trim($allowedIps); $allowedIps = trim($allowedIps);
if ($allowedIps === '' || strtolower($allowedIps) === 'kaikki') return true; if ($allowedIps === '' || strtolower($allowedIps) === 'kaikki') return true;
$entries = preg_split('/[\s,]+/', $allowedIps, -1, PREG_SPLIT_NO_EMPTY); $entries = preg_split('/[\s,]+/', $allowedIps, -1, PREG_SPLIT_NO_EMPTY);
// Normalisoi IP: IPv4-mapped IPv6 (::ffff:1.2.3.4) → myös IPv4 muotoon
$ipBin = @inet_pton($ip); $ipBin = @inet_pton($ip);
if ($ipBin === false) return false; if ($ipBin === false) return false;
// Jos IP on IPv4-mapped IPv6 (::ffff:x.x.x.x), kokeile myös puhtaana IPv4:nä
$ipv4Equivalent = null;
if (strlen($ipBin) === 16 && substr($ipBin, 0, 12) === "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff") {
$ipv4Equivalent = inet_ntop(substr($ipBin, 12));
}
// Jos IP on puhdas IPv4, kokeile myös mapped-muodossa
$ipv6MappedBin = null;
if (strlen($ipBin) === 4) {
$ipv6MappedBin = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff" . $ipBin;
}
foreach ($entries as $entry) { foreach ($entries as $entry) {
$entry = trim($entry); $entry = trim($entry);
if ($entry === '') continue; if ($entry === '') continue;
@@ -157,19 +171,34 @@ function isIpAllowed(string $ip, string $allowedIps): bool {
$bits = (int)$bits; $bits = (int)$bits;
$subnetBin = @inet_pton($subnet); $subnetBin = @inet_pton($subnet);
if ($subnetBin === false) continue; if ($subnetBin === false) continue;
// IPv4 = 4 tavua, IPv6 = 16 tavua — pitää olla sama perhe $maxBits = strlen($subnetBin) * 8;
if (strlen($ipBin) !== strlen($subnetBin)) continue;
$maxBits = strlen($ipBin) * 8;
if ($bits < 0 || $bits > $maxBits) continue; if ($bits < 0 || $bits > $maxBits) continue;
// Rakenna bittimask // Rakenna bittimask
$mask = str_repeat("\xff", intdiv($bits, 8)); $mask = str_repeat("\xff", intdiv($bits, 8));
if ($bits % 8) $mask .= chr(0xff << (8 - ($bits % 8))); if ($bits % 8) $mask .= chr(0xff << (8 - ($bits % 8)));
$mask = str_pad($mask, strlen($ipBin), "\x00"); $mask = str_pad($mask, strlen($subnetBin), "\x00");
if (($ipBin & $mask) === ($subnetBin & $mask)) return true;
// Tarkista suoraan
if (strlen($ipBin) === strlen($subnetBin) && ($ipBin & $mask) === ($subnetBin & $mask)) return true;
// Tarkista IPv4-equivalent
if ($ipv4Equivalent) {
$ipv4Bin = @inet_pton($ipv4Equivalent);
if ($ipv4Bin && strlen($ipv4Bin) === strlen($subnetBin) && ($ipv4Bin & $mask) === ($subnetBin & $mask)) return true;
}
// Tarkista IPv6-mapped
if ($ipv6MappedBin && strlen($ipv6MappedBin) === strlen($subnetBin) && ($ipv6MappedBin & $mask) === ($subnetBin & $mask)) return true;
} else { } else {
// Yksittäinen IP (IPv4 tai IPv6) // Yksittäinen IP (IPv4 tai IPv6)
$entryBin = @inet_pton($entry); $entryBin = @inet_pton($entry);
if ($entryBin !== false && $ipBin === $entryBin) return true; if ($entryBin === false) continue;
if ($ipBin === $entryBin) return true;
// Tarkista IPv4-equivalent
if ($ipv4Equivalent) {
$ipv4Bin = @inet_pton($ipv4Equivalent);
if ($ipv4Bin && $ipv4Bin === $entryBin) return true;
}
// Tarkista IPv6-mapped
if ($ipv6MappedBin && $ipv6MappedBin === $entryBin) return true;
} }
} }
return false; return false;