From 7ab50248dc12c43cd920af283b554120d9496963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 27 Jun 2022 16:48:58 +0200 Subject: [PATCH 1/8] Improve local IP detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- lib/private/Http/Client/LocalAddressChecker.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/private/Http/Client/LocalAddressChecker.php b/lib/private/Http/Client/LocalAddressChecker.php index c69d1007a160e..b233f34b19c63 100644 --- a/lib/private/Http/Client/LocalAddressChecker.php +++ b/lib/private/Http/Client/LocalAddressChecker.php @@ -41,6 +41,12 @@ public function ThrowIfLocalIp(string $ip) : void { throw new LocalServerException('Host violates local access rules'); } + $localIps = ['100.100.100.200']; + if ((bool)filter_var($ip, FILTER_VALIDATE_IP) && in_array($ip, $localIps)) { + $this->logger->warning("Host $ip was not connected to because it violates local access rules"); + throw new LocalServerException('Host violates local access rules'); + } + // Also check for IPv6 IPv4 nesting, because that's not covered by filter_var if ((bool)filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && substr_count($ip, '.') > 0) { $delimiter = strrpos($ip, ':'); // Get last colon From 1d0ddcfca1b5c8391ee910dbb7127c94b8082e74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Tue, 28 Jun 2022 11:35:45 +0200 Subject: [PATCH 2/8] Refactor local IP if and set strict to true for in_array MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- lib/private/Http/Client/LocalAddressChecker.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/private/Http/Client/LocalAddressChecker.php b/lib/private/Http/Client/LocalAddressChecker.php index b233f34b19c63..749a1a97c8acf 100644 --- a/lib/private/Http/Client/LocalAddressChecker.php +++ b/lib/private/Http/Client/LocalAddressChecker.php @@ -36,13 +36,13 @@ public function __construct(LoggerInterface $logger) { } public function ThrowIfLocalIp(string $ip) : void { - if ((bool)filter_var($ip, FILTER_VALIDATE_IP) && !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { - $this->logger->warning("Host $ip was not connected to because it violates local access rules"); - throw new LocalServerException('Host violates local access rules'); - } - $localIps = ['100.100.100.200']; - if ((bool)filter_var($ip, FILTER_VALIDATE_IP) && in_array($ip, $localIps)) { + if ( + (bool)filter_var($ip, FILTER_VALIDATE_IP) && + ( + !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) || + in_array($ip, $localIps, true) + )) { $this->logger->warning("Host $ip was not connected to because it violates local access rules"); throw new LocalServerException('Host violates local access rules'); } From 8cddb75c2b3b34b9062ddc9c02533e502d1dc62f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Tue, 28 Jun 2022 16:48:38 +0200 Subject: [PATCH 3/8] Check for local IPs nested in IPv6 as well MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- lib/private/Http/Client/LocalAddressChecker.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/private/Http/Client/LocalAddressChecker.php b/lib/private/Http/Client/LocalAddressChecker.php index 749a1a97c8acf..b0c420a4fe8ca 100644 --- a/lib/private/Http/Client/LocalAddressChecker.php +++ b/lib/private/Http/Client/LocalAddressChecker.php @@ -52,7 +52,9 @@ public function ThrowIfLocalIp(string $ip) : void { $delimiter = strrpos($ip, ':'); // Get last colon $ipv4Address = substr($ip, $delimiter + 1); - if (!filter_var($ipv4Address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { + if ( + !filter_var($ipv4Address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) || + in_array($ipv4Address, $localIps, true)) { $this->logger->warning("Host $ip was not connected to because it violates local access rules"); throw new LocalServerException('Host violates local access rules'); } From d0ceedf05400dc36fad016e05b88456895bfb410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Tue, 12 Jul 2022 12:09:05 +0200 Subject: [PATCH 4/8] Use Symfony IpUtils to check for local IP ranges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- lib/private/Http/Client/LocalAddressChecker.php | 10 +++++++--- tests/lib/Http/Client/LocalAddressCheckerTest.php | 5 +++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/private/Http/Client/LocalAddressChecker.php b/lib/private/Http/Client/LocalAddressChecker.php index b0c420a4fe8ca..f4fea503ab9d5 100644 --- a/lib/private/Http/Client/LocalAddressChecker.php +++ b/lib/private/Http/Client/LocalAddressChecker.php @@ -27,6 +27,7 @@ use OCP\Http\Client\LocalServerException; use Psr\Log\LoggerInterface; +use Symfony\Component\HttpFoundation\IpUtils; class LocalAddressChecker { private LoggerInterface $logger; @@ -36,12 +37,15 @@ public function __construct(LoggerInterface $logger) { } public function ThrowIfLocalIp(string $ip) : void { - $localIps = ['100.100.100.200']; + $localRanges = [ + '100.64.0.0/10', // See RFC 6598 + '192.0.0.0/24', // See RFC 6890 + ]; if ( (bool)filter_var($ip, FILTER_VALIDATE_IP) && ( !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) || - in_array($ip, $localIps, true) + IpUtils::checkIp($ip, $localRanges) )) { $this->logger->warning("Host $ip was not connected to because it violates local access rules"); throw new LocalServerException('Host violates local access rules'); @@ -54,7 +58,7 @@ public function ThrowIfLocalIp(string $ip) : void { if ( !filter_var($ipv4Address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) || - in_array($ipv4Address, $localIps, true)) { + IpUtils::checkIp($ip, $localRanges)) { $this->logger->warning("Host $ip was not connected to because it violates local access rules"); throw new LocalServerException('Host violates local access rules'); } diff --git a/tests/lib/Http/Client/LocalAddressCheckerTest.php b/tests/lib/Http/Client/LocalAddressCheckerTest.php index 0bba1cee5f4a2..9f2f6c72993e3 100644 --- a/tests/lib/Http/Client/LocalAddressCheckerTest.php +++ b/tests/lib/Http/Client/LocalAddressCheckerTest.php @@ -96,6 +96,8 @@ public function dataInternalIPs() : array { ['10.0.0.1'], ['::'], ['::1'], + ['100.100.100.200'], + ['192.0.0.1'], ]; } @@ -116,6 +118,9 @@ public function dataPreventLocalAddress():array { ['another-host.local'], ['service.localhost'], ['!@#$'], // test invalid url + ['100.100.100.200'], + ['192.0.0.1'], + ['randomdomain.internal'], ]; } From d3a2fd02c27b1e74547fb90ff77cbb9a5d1a5fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 1 Aug 2022 11:30:20 +0200 Subject: [PATCH 5/8] Copy IpUtils from Symfony to avoid new dependency on backport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- .../Http/Client/LocalAddressChecker.php | 2 +- lib/private/Http/IpUtils.php | 202 ++++++++++++++++++ 2 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 lib/private/Http/IpUtils.php diff --git a/lib/private/Http/Client/LocalAddressChecker.php b/lib/private/Http/Client/LocalAddressChecker.php index f4fea503ab9d5..da641b57c0417 100644 --- a/lib/private/Http/Client/LocalAddressChecker.php +++ b/lib/private/Http/Client/LocalAddressChecker.php @@ -27,7 +27,7 @@ use OCP\Http\Client\LocalServerException; use Psr\Log\LoggerInterface; -use Symfony\Component\HttpFoundation\IpUtils; +use OC\Http\IpUtils; class LocalAddressChecker { private LoggerInterface $logger; diff --git a/lib/private/Http/IpUtils.php b/lib/private/Http/IpUtils.php new file mode 100644 index 0000000000000..f1e8c59edb936 --- /dev/null +++ b/lib/private/Http/IpUtils.php @@ -0,0 +1,202 @@ + + * + * SPDX-License-Identifier: MIT + */ + +namespace OC\Http; + +/** + * Http utility functions. + * + * @author Fabien Potencier + */ +class IpUtils +{ + private static $checkedIps = []; + + /** + * This class should not be instantiated. + */ + private function __construct() + { + } + + /** + * Checks if an IPv4 or IPv6 address is contained in the list of given IPs or subnets. + * + * @param string|array $ips List of IPs or subnets (can be a string if only a single one) + * + * @return bool + */ + public static function checkIp(?string $requestIp, $ips) + { + if (null === $requestIp) { + trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + return false; + } + + if (!\is_array($ips)) { + $ips = [$ips]; + } + + $method = substr_count($requestIp, ':') > 1 ? 'checkIp6' : 'checkIp4'; + + foreach ($ips as $ip) { + if (self::$method($requestIp, $ip)) { + return true; + } + } + + return false; + } + + /** + * Compares two IPv4 addresses. + * In case a subnet is given, it checks if it contains the request IP. + * + * @param string $ip IPv4 address or subnet in CIDR notation + * + * @return bool Whether the request IP matches the IP, or whether the request IP is within the CIDR subnet + */ + public static function checkIp4(?string $requestIp, string $ip) + { + if (null === $requestIp) { + trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + return false; + } + + $cacheKey = $requestIp.'-'.$ip; + if (isset(self::$checkedIps[$cacheKey])) { + return self::$checkedIps[$cacheKey]; + } + + if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) { + return self::$checkedIps[$cacheKey] = false; + } + + if (str_contains($ip, '/')) { + [$address, $netmask] = explode('/', $ip, 2); + + if ('0' === $netmask) { + return self::$checkedIps[$cacheKey] = filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4); + } + + if ($netmask < 0 || $netmask > 32) { + return self::$checkedIps[$cacheKey] = false; + } + } else { + $address = $ip; + $netmask = 32; + } + + if (false === ip2long($address)) { + return self::$checkedIps[$cacheKey] = false; + } + + return self::$checkedIps[$cacheKey] = 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask); + } + + /** + * Compares two IPv6 addresses. + * In case a subnet is given, it checks if it contains the request IP. + * + * @author David Soria Parra + * + * @see https://github.com/dsp/v6tools + * + * @param string $ip IPv6 address or subnet in CIDR notation + * + * @return bool + * + * @throws \RuntimeException When IPV6 support is not enabled + */ + public static function checkIp6(?string $requestIp, string $ip) + { + if (null === $requestIp) { + trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + return false; + } + + $cacheKey = $requestIp.'-'.$ip; + if (isset(self::$checkedIps[$cacheKey])) { + return self::$checkedIps[$cacheKey]; + } + + if (!((\extension_loaded('sockets') && \defined('AF_INET6')) || @inet_pton('::1'))) { + throw new \RuntimeException('Unable to check Ipv6. Check that PHP was not compiled with option "disable-ipv6".'); + } + + if (str_contains($ip, '/')) { + [$address, $netmask] = explode('/', $ip, 2); + + if ('0' === $netmask) { + return (bool) unpack('n*', @inet_pton($address)); + } + + if ($netmask < 1 || $netmask > 128) { + return self::$checkedIps[$cacheKey] = false; + } + } else { + $address = $ip; + $netmask = 128; + } + + $bytesAddr = unpack('n*', @inet_pton($address)); + $bytesTest = unpack('n*', @inet_pton($requestIp)); + + if (!$bytesAddr || !$bytesTest) { + return self::$checkedIps[$cacheKey] = false; + } + + for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) { + $left = $netmask - 16 * ($i - 1); + $left = ($left <= 16) ? $left : 16; + $mask = ~(0xFFFF >> $left) & 0xFFFF; + if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) { + return self::$checkedIps[$cacheKey] = false; + } + } + + return self::$checkedIps[$cacheKey] = true; + } + + /** + * Anonymizes an IP/IPv6. + * + * Removes the last byte for v4 and the last 8 bytes for v6 IPs + */ + public static function anonymize(string $ip): string + { + $wrappedIPv6 = false; + if ('[' === substr($ip, 0, 1) && ']' === substr($ip, -1, 1)) { + $wrappedIPv6 = true; + $ip = substr($ip, 1, -1); + } + + $packedAddress = inet_pton($ip); + if (4 === \strlen($packedAddress)) { + $mask = '255.255.255.0'; + } elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff:ffff'))) { + $mask = '::ffff:ffff:ff00'; + } elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff'))) { + $mask = '::ffff:ff00'; + } else { + $mask = 'ffff:ffff:ffff:ffff:0000:0000:0000:0000'; + } + $ip = inet_ntop($packedAddress & inet_pton($mask)); + + if ($wrappedIPv6) { + $ip = '['.$ip.']'; + } + + return $ip; + } +} From ead97ba46d6a90cd52dfbf2323460ecf5a7fbf5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 1 Aug 2022 11:34:28 +0200 Subject: [PATCH 6/8] Align copied file with our code style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- lib/private/Http/IpUtils.php | 362 +++++++++++++++++------------------ 1 file changed, 178 insertions(+), 184 deletions(-) diff --git a/lib/private/Http/IpUtils.php b/lib/private/Http/IpUtils.php index f1e8c59edb936..cc2bba06dbb90 100644 --- a/lib/private/Http/IpUtils.php +++ b/lib/private/Http/IpUtils.php @@ -15,188 +15,182 @@ * * @author Fabien Potencier */ -class IpUtils -{ - private static $checkedIps = []; - - /** - * This class should not be instantiated. - */ - private function __construct() - { - } - - /** - * Checks if an IPv4 or IPv6 address is contained in the list of given IPs or subnets. - * - * @param string|array $ips List of IPs or subnets (can be a string if only a single one) - * - * @return bool - */ - public static function checkIp(?string $requestIp, $ips) - { - if (null === $requestIp) { - trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__); - - return false; - } - - if (!\is_array($ips)) { - $ips = [$ips]; - } - - $method = substr_count($requestIp, ':') > 1 ? 'checkIp6' : 'checkIp4'; - - foreach ($ips as $ip) { - if (self::$method($requestIp, $ip)) { - return true; - } - } - - return false; - } - - /** - * Compares two IPv4 addresses. - * In case a subnet is given, it checks if it contains the request IP. - * - * @param string $ip IPv4 address or subnet in CIDR notation - * - * @return bool Whether the request IP matches the IP, or whether the request IP is within the CIDR subnet - */ - public static function checkIp4(?string $requestIp, string $ip) - { - if (null === $requestIp) { - trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__); - - return false; - } - - $cacheKey = $requestIp.'-'.$ip; - if (isset(self::$checkedIps[$cacheKey])) { - return self::$checkedIps[$cacheKey]; - } - - if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) { - return self::$checkedIps[$cacheKey] = false; - } - - if (str_contains($ip, '/')) { - [$address, $netmask] = explode('/', $ip, 2); - - if ('0' === $netmask) { - return self::$checkedIps[$cacheKey] = filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4); - } - - if ($netmask < 0 || $netmask > 32) { - return self::$checkedIps[$cacheKey] = false; - } - } else { - $address = $ip; - $netmask = 32; - } - - if (false === ip2long($address)) { - return self::$checkedIps[$cacheKey] = false; - } - - return self::$checkedIps[$cacheKey] = 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask); - } - - /** - * Compares two IPv6 addresses. - * In case a subnet is given, it checks if it contains the request IP. - * - * @author David Soria Parra - * - * @see https://github.com/dsp/v6tools - * - * @param string $ip IPv6 address or subnet in CIDR notation - * - * @return bool - * - * @throws \RuntimeException When IPV6 support is not enabled - */ - public static function checkIp6(?string $requestIp, string $ip) - { - if (null === $requestIp) { - trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__); - - return false; - } - - $cacheKey = $requestIp.'-'.$ip; - if (isset(self::$checkedIps[$cacheKey])) { - return self::$checkedIps[$cacheKey]; - } - - if (!((\extension_loaded('sockets') && \defined('AF_INET6')) || @inet_pton('::1'))) { - throw new \RuntimeException('Unable to check Ipv6. Check that PHP was not compiled with option "disable-ipv6".'); - } - - if (str_contains($ip, '/')) { - [$address, $netmask] = explode('/', $ip, 2); - - if ('0' === $netmask) { - return (bool) unpack('n*', @inet_pton($address)); - } - - if ($netmask < 1 || $netmask > 128) { - return self::$checkedIps[$cacheKey] = false; - } - } else { - $address = $ip; - $netmask = 128; - } - - $bytesAddr = unpack('n*', @inet_pton($address)); - $bytesTest = unpack('n*', @inet_pton($requestIp)); - - if (!$bytesAddr || !$bytesTest) { - return self::$checkedIps[$cacheKey] = false; - } - - for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) { - $left = $netmask - 16 * ($i - 1); - $left = ($left <= 16) ? $left : 16; - $mask = ~(0xFFFF >> $left) & 0xFFFF; - if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) { - return self::$checkedIps[$cacheKey] = false; - } - } - - return self::$checkedIps[$cacheKey] = true; - } - - /** - * Anonymizes an IP/IPv6. - * - * Removes the last byte for v4 and the last 8 bytes for v6 IPs - */ - public static function anonymize(string $ip): string - { - $wrappedIPv6 = false; - if ('[' === substr($ip, 0, 1) && ']' === substr($ip, -1, 1)) { - $wrappedIPv6 = true; - $ip = substr($ip, 1, -1); - } - - $packedAddress = inet_pton($ip); - if (4 === \strlen($packedAddress)) { - $mask = '255.255.255.0'; - } elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff:ffff'))) { - $mask = '::ffff:ffff:ff00'; - } elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff'))) { - $mask = '::ffff:ff00'; - } else { - $mask = 'ffff:ffff:ffff:ffff:0000:0000:0000:0000'; - } - $ip = inet_ntop($packedAddress & inet_pton($mask)); - - if ($wrappedIPv6) { - $ip = '['.$ip.']'; - } - - return $ip; - } +class IpUtils { + private static $checkedIps = []; + + /** + * This class should not be instantiated. + */ + private function __construct() { + } + + /** + * Checks if an IPv4 or IPv6 address is contained in the list of given IPs or subnets. + * + * @param string|array $ips List of IPs or subnets (can be a string if only a single one) + * + * @return bool + */ + public static function checkIp(?string $requestIp, $ips) { + if (null === $requestIp) { + trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + return false; + } + + if (!\is_array($ips)) { + $ips = [$ips]; + } + + $method = substr_count($requestIp, ':') > 1 ? 'checkIp6' : 'checkIp4'; + + foreach ($ips as $ip) { + if (self::$method($requestIp, $ip)) { + return true; + } + } + + return false; + } + + /** + * Compares two IPv4 addresses. + * In case a subnet is given, it checks if it contains the request IP. + * + * @param string $ip IPv4 address or subnet in CIDR notation + * + * @return bool Whether the request IP matches the IP, or whether the request IP is within the CIDR subnet + */ + public static function checkIp4(?string $requestIp, string $ip) { + if (null === $requestIp) { + trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + return false; + } + + $cacheKey = $requestIp.'-'.$ip; + if (isset(self::$checkedIps[$cacheKey])) { + return self::$checkedIps[$cacheKey]; + } + + if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) { + return self::$checkedIps[$cacheKey] = false; + } + + if (str_contains($ip, '/')) { + [$address, $netmask] = explode('/', $ip, 2); + + if ('0' === $netmask) { + return self::$checkedIps[$cacheKey] = filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4); + } + + if ($netmask < 0 || $netmask > 32) { + return self::$checkedIps[$cacheKey] = false; + } + } else { + $address = $ip; + $netmask = 32; + } + + if (false === ip2long($address)) { + return self::$checkedIps[$cacheKey] = false; + } + + return self::$checkedIps[$cacheKey] = 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask); + } + + /** + * Compares two IPv6 addresses. + * In case a subnet is given, it checks if it contains the request IP. + * + * @author David Soria Parra + * + * @see https://github.com/dsp/v6tools + * + * @param string $ip IPv6 address or subnet in CIDR notation + * + * @return bool + * + * @throws \RuntimeException When IPV6 support is not enabled + */ + public static function checkIp6(?string $requestIp, string $ip) { + if (null === $requestIp) { + trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + return false; + } + + $cacheKey = $requestIp.'-'.$ip; + if (isset(self::$checkedIps[$cacheKey])) { + return self::$checkedIps[$cacheKey]; + } + + if (!((\extension_loaded('sockets') && \defined('AF_INET6')) || @inet_pton('::1'))) { + throw new \RuntimeException('Unable to check Ipv6. Check that PHP was not compiled with option "disable-ipv6".'); + } + + if (str_contains($ip, '/')) { + [$address, $netmask] = explode('/', $ip, 2); + + if ('0' === $netmask) { + return (bool) unpack('n*', @inet_pton($address)); + } + + if ($netmask < 1 || $netmask > 128) { + return self::$checkedIps[$cacheKey] = false; + } + } else { + $address = $ip; + $netmask = 128; + } + + $bytesAddr = unpack('n*', @inet_pton($address)); + $bytesTest = unpack('n*', @inet_pton($requestIp)); + + if (!$bytesAddr || !$bytesTest) { + return self::$checkedIps[$cacheKey] = false; + } + + for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) { + $left = $netmask - 16 * ($i - 1); + $left = ($left <= 16) ? $left : 16; + $mask = ~(0xFFFF >> $left) & 0xFFFF; + if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) { + return self::$checkedIps[$cacheKey] = false; + } + } + + return self::$checkedIps[$cacheKey] = true; + } + + /** + * Anonymizes an IP/IPv6. + * + * Removes the last byte for v4 and the last 8 bytes for v6 IPs + */ + public static function anonymize(string $ip): string { + $wrappedIPv6 = false; + if ('[' === substr($ip, 0, 1) && ']' === substr($ip, -1, 1)) { + $wrappedIPv6 = true; + $ip = substr($ip, 1, -1); + } + + $packedAddress = inet_pton($ip); + if (4 === \strlen($packedAddress)) { + $mask = '255.255.255.0'; + } elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff:ffff'))) { + $mask = '::ffff:ffff:ff00'; + } elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff'))) { + $mask = '::ffff:ff00'; + } else { + $mask = 'ffff:ffff:ffff:ffff:0000:0000:0000:0000'; + } + $ip = inet_ntop($packedAddress & inet_pton($mask)); + + if ($wrappedIPv6) { + $ip = '['.$ip.']'; + } + + return $ip; + } } From 5daf307a0d27ca6539443a820bb1d9059d07d14a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 1 Aug 2022 12:09:23 +0200 Subject: [PATCH 7/8] Remove call to undefined function, fix typing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- lib/private/Http/IpUtils.php | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/lib/private/Http/IpUtils.php b/lib/private/Http/IpUtils.php index cc2bba06dbb90..97a3cb9dba1a8 100644 --- a/lib/private/Http/IpUtils.php +++ b/lib/private/Http/IpUtils.php @@ -16,6 +16,9 @@ * @author Fabien Potencier */ class IpUtils { + /** + * @var array + */ private static $checkedIps = []; /** @@ -31,13 +34,7 @@ private function __construct() { * * @return bool */ - public static function checkIp(?string $requestIp, $ips) { - if (null === $requestIp) { - trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__); - - return false; - } - + public static function checkIp(string $requestIp, $ips) { if (!\is_array($ips)) { $ips = [$ips]; } @@ -61,13 +58,7 @@ public static function checkIp(?string $requestIp, $ips) { * * @return bool Whether the request IP matches the IP, or whether the request IP is within the CIDR subnet */ - public static function checkIp4(?string $requestIp, string $ip) { - if (null === $requestIp) { - trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__); - - return false; - } - + public static function checkIp4(string $requestIp, string $ip) { $cacheKey = $requestIp.'-'.$ip; if (isset(self::$checkedIps[$cacheKey])) { return self::$checkedIps[$cacheKey]; @@ -81,9 +72,11 @@ public static function checkIp4(?string $requestIp, string $ip) { [$address, $netmask] = explode('/', $ip, 2); if ('0' === $netmask) { - return self::$checkedIps[$cacheKey] = filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4); + return self::$checkedIps[$cacheKey] = (bool)filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4); } + $netmask = (int)$netmask; + if ($netmask < 0 || $netmask > 32) { return self::$checkedIps[$cacheKey] = false; } @@ -113,13 +106,7 @@ public static function checkIp4(?string $requestIp, string $ip) { * * @throws \RuntimeException When IPV6 support is not enabled */ - public static function checkIp6(?string $requestIp, string $ip) { - if (null === $requestIp) { - trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__); - - return false; - } - + public static function checkIp6(string $requestIp, string $ip) { $cacheKey = $requestIp.'-'.$ip; if (isset(self::$checkedIps[$cacheKey])) { return self::$checkedIps[$cacheKey]; @@ -136,6 +123,8 @@ public static function checkIp6(?string $requestIp, string $ip) { return (bool) unpack('n*', @inet_pton($address)); } + $netmask = (int)$netmask; + if ($netmask < 1 || $netmask > 128) { return self::$checkedIps[$cacheKey] = false; } From 38eb0df6714933eaee94ea0abc619ce5715eaaa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 1 Aug 2022 15:13:34 +0200 Subject: [PATCH 8/8] Fix autoloader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index b3b31cae4cc0a..59939b9cb1fda 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1251,6 +1251,7 @@ 'OC\\Http\\Client\\NegativeDnsCache' => $baseDir . '/lib/private/Http/Client/NegativeDnsCache.php', 'OC\\Http\\Client\\Response' => $baseDir . '/lib/private/Http/Client/Response.php', 'OC\\Http\\CookieHelper' => $baseDir . '/lib/private/Http/CookieHelper.php', + 'OC\\Http\\IpUtils' => $baseDir . '/lib/private/Http/IpUtils.php', 'OC\\Http\\WellKnown\\RequestManager' => $baseDir . '/lib/private/Http/WellKnown/RequestManager.php', 'OC\\InitialStateService' => $baseDir . '/lib/private/InitialStateService.php', 'OC\\Installer' => $baseDir . '/lib/private/Installer.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index fd2d072e9a876..57ba09046b30c 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1280,6 +1280,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Http\\Client\\NegativeDnsCache' => __DIR__ . '/../../..' . '/lib/private/Http/Client/NegativeDnsCache.php', 'OC\\Http\\Client\\Response' => __DIR__ . '/../../..' . '/lib/private/Http/Client/Response.php', 'OC\\Http\\CookieHelper' => __DIR__ . '/../../..' . '/lib/private/Http/CookieHelper.php', + 'OC\\Http\\IpUtils' => __DIR__ . '/../../..' . '/lib/private/Http/IpUtils.php', 'OC\\Http\\WellKnown\\RequestManager' => __DIR__ . '/../../..' . '/lib/private/Http/WellKnown/RequestManager.php', 'OC\\InitialStateService' => __DIR__ . '/../../..' . '/lib/private/InitialStateService.php', 'OC\\Installer' => __DIR__ . '/../../..' . '/lib/private/Installer.php',