Skip to content

Commit 7929977

Browse files
Merge pull request #62827 from nextcloud/backport/62811/stable32
[stable32] Host/ip validator hardenings
2 parents 9e67872 + ac326cd commit 7929977

5 files changed

Lines changed: 79 additions & 4 deletions

File tree

lib/private/Net/HostnameClassifier.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class HostnameClassifier {
3838
* IP addresses are not considered local. Use the IpAddressClassifier for those.
3939
*/
4040
public function isLocalHostname(string $hostname): bool {
41+
$hostname = rtrim($hostname, '.');
4142
// Disallow local network top-level domains from RFC 6762
4243
$topLevelDomain = substr((strrchr($hostname, '.') ?: ''), 1);
4344
if (in_array($topLevelDomain, self::LOCAL_TOPLEVEL_DOMAINS)) {

lib/private/Net/IpAddressClassifier.php

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
use IPLib\Address\IPv6;
1414
use IPLib\Factory;
1515
use IPLib\ParseStringFlag;
16+
use IPLib\Range\RangeInterface;
17+
use IPLib\Range\Subnet;
1618
use Symfony\Component\HttpFoundation\IpUtils;
1719
use function filter_var;
1820

@@ -27,12 +29,48 @@ class IpAddressClassifier {
2729
'192.0.0.0/24', // See RFC 6890
2830
];
2931

32+
private RangeInterface $nat64Range;
33+
private RangeInterface $rfc8215;
34+
private RangeInterface $teredo;
35+
private RangeInterface $ipv4Compatible;
36+
37+
public function __construct() {
38+
$this->nat64Range = Subnet::parseString('64:ff9b::/96');
39+
$this->rfc8215 = Subnet::parseString('64:ff9b:1::/48');
40+
$this->teredo = Subnet::parseString('2001::/32');
41+
$this->ipv4Compatible = Subnet::parseString('::0:0/96');
42+
}
43+
44+
/**
45+
* Get the ipv4 that an ipv6 address maps to, if any.
46+
*
47+
* Note that this is not just ipv6 representations of ipv4 addresses,
48+
* but also any NAT or proxy style translation addresses
49+
*/
50+
public function getMappedIpv4(IPv6 $ip): ?IPv4 {
51+
$ipv4 = $ip->toIPv4();
52+
$ipv6Bytes = $ip->getBytes();
53+
if ($ipv4) {
54+
return $ipv4;
55+
} elseif ($this->nat64Range->contains($ip)) {
56+
return IPv4::fromBytes(array_slice($ipv6Bytes, -4, 4));
57+
} elseif ($this->ipv4Compatible->contains($ip)) {
58+
return IPv4::fromBytes(array_slice($ipv6Bytes, -4, 4));
59+
} elseif ($this->teredo->contains($ip)) {
60+
$xorBytes = array_slice($ipv6Bytes, -4, 4);
61+
return IPv4::fromBytes(array_map(fn (int $byte) => $byte ^ 0xFF, $xorBytes));
62+
}
63+
64+
return null;
65+
}
66+
3067
/**
3168
* Check host identifier for local IPv4 and IPv6 address ranges
3269
*
3370
* Hostnames are not considered local. Use the HostnameClassifier for those.
3471
*/
3572
public function isLocalAddress(string $ip): bool {
73+
$ip = rtrim($ip, '.');
3674
$parsedIp = Factory::parseAddressString(
3775
$ip,
3876
ParseStringFlag::IPV4_MAYBE_NON_DECIMAL | ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED | ParseStringFlag::MAY_INCLUDE_ZONEID
@@ -43,12 +81,16 @@ public function isLocalAddress(string $ip): bool {
4381
}
4482
/* Replace by normalized form */
4583
if ($parsedIp instanceof IPv6) {
46-
$ipv4 = $parsedIp->toIPv4();
47-
$ipv6Bytes = $parsedIp->getBytes();
84+
// rfc8215 is a generic reservation for ipv6/ipv4 translation mechanisms,
85+
// no assumptions can be made about how ipv4 addresses are encoded within.
86+
//
87+
// Thus the only thing we can do is treat them all as local
88+
if ($this->rfc8215->contains($parsedIp)) {
89+
return true;
90+
}
91+
$ipv4 = $this->getMappedIpv4($parsedIp);
4892
if ($ipv4) {
4993
$ip = (string)$ipv4;
50-
} elseif (array_slice($ipv6Bytes, 0, 4) === [0x00, 0x64, 0xFF, 0x9B]) {
51-
$ip = (string)IPv4::fromBytes(array_slice($ipv6Bytes, -4, 4));
5294
} else {
5395
$ip = (string)$parsedIp;
5496
}

lib/private/Security/RemoteHostValidator.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ public function isValid(string $host): bool {
3535
return true;
3636
}
3737

38+
$host = rtrim($host, '.');
39+
3840
$host = idn_to_utf8(strtolower(urldecode($host)));
3941
if ($host === false) {
4042
return false;

tests/lib/Net/HostnameClassifierTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public static function localHostnamesData(): array {
2929
['another-host.local'],
3030
['service.localhost'],
3131
['randomdomain.internal'],
32+
['another-host.local.'],
3233
];
3334
}
3435

@@ -46,6 +47,7 @@ public static function publicHostnamesData(): array {
4647
['example.org'],
4748
['host.domain'],
4849
['cloud.domain.tld'],
50+
['cloud.domain.tld.'],
4951
];
5052
}
5153

tests/lib/Net/IpAddressClassifierTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
namespace lib\Net;
1111

12+
use IPLib\Address\IPv4;
13+
use IPLib\Address\IPv6;
1214
use OC\Net\IpAddressClassifier;
1315
use Test\TestCase;
1416

@@ -50,6 +52,10 @@ public static function localIpAddressData(): array {
5052
['100.100.100.200'],
5153
['192.0.0.1'],
5254
['64:ff9b::a9fe:a9fe'], // NAT64 of 169.254.169.254
55+
['::ffff:127.0.0.1'],
56+
['2130706433'],
57+
['0177.0.0.1'],
58+
['169.254.169.254'],
5359
];
5460
}
5561

@@ -59,4 +65,26 @@ public function testLocalAddress(string $ip): void {
5965

6066
self::assertTrue($isLocal);
6167
}
68+
69+
public static function mappedAddresses(): array {
70+
return [
71+
['64:ff9b::a9fe:a9fe', '169.254.169.254'],
72+
['::ffff:7f00:1', '127.0.0.1'],
73+
['::127.0.0.1', '127.0.0.1'],
74+
['::7f00:1', '127.0.0.1'],
75+
['2001:0000:4136:e378:8000:63bf:3fff:fdd2', '192.0.2.45'],
76+
['2001:4860:4860::8888', null],
77+
];
78+
}
79+
80+
#[\PHPUnit\Framework\Attributes\DataProvider('mappedAddresses')]
81+
public function testMappedAddresses(string $ipv6, ?string $ipv4): void {
82+
$mapped = $this->classifier->getMappedIpv4(IPv6::parseString($ipv6));
83+
84+
if ($ipv4 === null) {
85+
self::assertEquals(null, $mapped);
86+
} else {
87+
self::assertEquals(IPv4::parseString($ipv4), $mapped);
88+
}
89+
}
6290
}

0 commit comments

Comments
 (0)