mirror of https://github.com/nextcloud/server.git
Add remote host validation API
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>pull/34852/head
parent
aa81b87f26
commit
8aea25b5b9
@ -1,102 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2021, Lukas Reschke <lukas@statuscode.ch>
|
||||
*
|
||||
* @author Lukas Reschke <lukas@statuscode.ch>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
namespace OC\Http\Client;
|
||||
|
||||
use IPLib\Address\IPv6;
|
||||
use IPLib\Factory;
|
||||
use IPLib\ParseStringFlag;
|
||||
use OCP\Http\Client\LocalServerException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\IpUtils;
|
||||
|
||||
class LocalAddressChecker {
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public function __construct(LoggerInterface $logger) {
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function throwIfLocalIp(string $ip) : void {
|
||||
$parsedIp = Factory::parseAddressString(
|
||||
$ip,
|
||||
ParseStringFlag::IPV4_MAYBE_NON_DECIMAL | ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED
|
||||
);
|
||||
if ($parsedIp === null) {
|
||||
/* Not an IP */
|
||||
return;
|
||||
}
|
||||
/* Replace by normalized form */
|
||||
if ($parsedIp instanceof IPv6) {
|
||||
$ip = (string)($parsedIp->toIPv4() ?? $parsedIp);
|
||||
} else {
|
||||
$ip = (string)$parsedIp;
|
||||
}
|
||||
|
||||
$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) ||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
public function throwIfLocalAddress(string $uri) : void {
|
||||
$host = parse_url($uri, PHP_URL_HOST);
|
||||
if ($host === false || $host === null) {
|
||||
$this->logger->warning("Could not detect any host in $uri");
|
||||
throw new LocalServerException('Could not detect any host');
|
||||
}
|
||||
|
||||
$host = idn_to_utf8(strtolower(urldecode($host)));
|
||||
// Remove brackets from IPv6 addresses
|
||||
if (strpos($host, '[') === 0 && substr($host, -1) === ']') {
|
||||
$host = substr($host, 1, -1);
|
||||
}
|
||||
|
||||
// Disallow local network top-level domains from RFC 6762
|
||||
$localTopLevelDomains = ['local','localhost','intranet','internal','private','corp','home','lan'];
|
||||
$topLevelDomain = substr((strrchr($host, '.') ?: ''), 1);
|
||||
if (in_array($topLevelDomain, $localTopLevelDomains)) {
|
||||
$this->logger->warning("Host $host was not connected to because it violates local access rules");
|
||||
throw new LocalServerException('Host violates local access rules');
|
||||
}
|
||||
|
||||
// Disallow hostname only
|
||||
if (substr_count($host, '.') === 0 && !(bool)filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$this->logger->warning("Host $host was not connected to because it violates local access rules");
|
||||
throw new LocalServerException('Host violates local access rules');
|
||||
}
|
||||
|
||||
$this->throwIfLocalIp($host);
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* @copyright 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace OC\Net;
|
||||
|
||||
use function filter_var;
|
||||
use function in_array;
|
||||
use function strrchr;
|
||||
use function substr;
|
||||
use function substr_count;
|
||||
|
||||
/**
|
||||
* Classifier for network hostnames
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class HostnameClassifier {
|
||||
private const LOCAL_TOPLEVEL_DOMAINS = [
|
||||
'local',
|
||||
'localhost',
|
||||
'intranet',
|
||||
'internal',
|
||||
'private',
|
||||
'corp',
|
||||
'home',
|
||||
'lan',
|
||||
];
|
||||
|
||||
/**
|
||||
* Check host identifier for local hostname
|
||||
*
|
||||
* IP addresses are not considered local. Use the IpAddressClassifier for those.
|
||||
*
|
||||
* @param string $hostname
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLocalHostname(string $hostname): bool {
|
||||
// Disallow local network top-level domains from RFC 6762
|
||||
$topLevelDomain = substr((strrchr($hostname, '.') ?: ''), 1);
|
||||
if (in_array($topLevelDomain, self::LOCAL_TOPLEVEL_DOMAINS)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Disallow hostname only
|
||||
if (substr_count($hostname, '.') === 0 && !filter_var($hostname, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* @copyright 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace OC\Net;
|
||||
|
||||
use IPLib\Address\IPv6;
|
||||
use IPLib\Factory;
|
||||
use IPLib\ParseStringFlag;
|
||||
use Symfony\Component\HttpFoundation\IpUtils;
|
||||
use function filter_var;
|
||||
|
||||
/**
|
||||
* Classifier for IP addresses
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class IpAddressClassifier {
|
||||
private const LOCAL_ADDRESS_RANGES = [
|
||||
'100.64.0.0/10', // See RFC 6598
|
||||
'192.0.0.0/24', // See RFC 6890
|
||||
];
|
||||
|
||||
/**
|
||||
* Check host identifier for local IPv4 and IPv6 address ranges
|
||||
*
|
||||
* Hostnames are not considered local. Use the HostnameClassifier for those.
|
||||
*
|
||||
* @param string $ip
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLocalAddress(string $ip): bool {
|
||||
$parsedIp = Factory::parseAddressString(
|
||||
$ip,
|
||||
ParseStringFlag::IPV4_MAYBE_NON_DECIMAL | ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED
|
||||
);
|
||||
if ($parsedIp === null) {
|
||||
/* Not an IP */
|
||||
return false;
|
||||
}
|
||||
/* Replace by normalized form */
|
||||
if ($parsedIp instanceof IPv6) {
|
||||
$ip = (string)($parsedIp->toIPv4() ?? $parsedIp);
|
||||
} else {
|
||||
$ip = (string)$parsedIp;
|
||||
}
|
||||
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
||||
/* Range address */
|
||||
return true;
|
||||
}
|
||||
if (IpUtils::checkIp($ip, self::LOCAL_ADDRESS_RANGES)) {
|
||||
/* Within local range */
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* @copyright 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace OC\Security;
|
||||
|
||||
use OC\Net\HostnameClassifier;
|
||||
use OC\Net\IpAddressClassifier;
|
||||
use OCP\IConfig;
|
||||
use OCP\Security\IRemoteHostValidator;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
use function urldecode;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class RemoteHostValidator implements IRemoteHostValidator {
|
||||
private IConfig $config;
|
||||
private HostnameClassifier $hostnameClassifier;
|
||||
private IpAddressClassifier $ipAddressClassifier;
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public function __construct(IConfig $config,
|
||||
HostnameClassifier $hostnameClassifier,
|
||||
IpAddressClassifier $ipAddressClassifier,
|
||||
LoggerInterface $logger) {
|
||||
$this->config = $config;
|
||||
$this->hostnameClassifier = $hostnameClassifier;
|
||||
$this->ipAddressClassifier = $ipAddressClassifier;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function isValid(string $host): bool {
|
||||
if ($this->config->getSystemValueBool('allow_local_remote_servers', false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$host = idn_to_utf8(strtolower(urldecode($host)));
|
||||
// Remove brackets from IPv6 addresses
|
||||
if (strpos($host, '[') === 0 && substr($host, -1) === ']') {
|
||||
$host = substr($host, 1, -1);
|
||||
}
|
||||
|
||||
if ($this->hostnameClassifier->isLocalHostname($host)
|
||||
|| $this->ipAddressClassifier->isLocalAddress($host)) {
|
||||
$this->logger->warning("Host $host was not connected to because it violates local access rules");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* @copyright 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace OCP\Security;
|
||||
|
||||
/**
|
||||
* Validator for remote hosts
|
||||
*
|
||||
* @since 26.0.0
|
||||
*/
|
||||
interface IRemoteHostValidator {
|
||||
|
||||
/**
|
||||
* Validate if a host may be connected to
|
||||
*
|
||||
* By default, Nextcloud does not connect to any local servers. That is neither
|
||||
* localhost nor any host in the local network.
|
||||
*
|
||||
* Admins can overwrite this behavior with the global `allow_local_remote_servers`
|
||||
* settings flag. If the flag is set to `true`, local hosts will be considered
|
||||
* valid.
|
||||
*
|
||||
* @param string $host hostname of the remote server, IPv4 or IPv6 address
|
||||
*
|
||||
* @return bool
|
||||
* @since 26.0.0
|
||||
*/
|
||||
public function isValid(string $host): bool;
|
||||
}
|
@ -1,158 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2021, Lukas Reschke <lukas@statuscode.ch>
|
||||
*
|
||||
* @author Lukas Reschke <lukas@statuscode.ch>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Test\Http\Client;
|
||||
|
||||
use OCP\Http\Client\LocalServerException;
|
||||
use OC\Http\Client\LocalAddressChecker;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class LocalAddressCheckerTest extends \Test\TestCase {
|
||||
/** @var LocalAddressChecker */
|
||||
private $localAddressChecker;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$this->localAddressChecker = new LocalAddressChecker($logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataPreventLocalAddress
|
||||
* @param string $uri
|
||||
*/
|
||||
public function testThrowIfLocalAddress($uri) : void {
|
||||
$this->expectException(LocalServerException::class);
|
||||
$this->localAddressChecker->throwIfLocalAddress('http://' . $uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataAllowLocalAddress
|
||||
* @param string $uri
|
||||
*/
|
||||
public function testThrowIfLocalAddressGood($uri) : void {
|
||||
$this->localAddressChecker->throwIfLocalAddress('http://' . $uri);
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @dataProvider dataInternalIPs
|
||||
* @param string $ip
|
||||
*/
|
||||
public function testThrowIfLocalIpBad($ip) : void {
|
||||
$this->expectException(LocalServerException::class);
|
||||
$this->localAddressChecker->throwIfLocalIp($ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataPublicIPs
|
||||
* @param string $ip
|
||||
*/
|
||||
public function testThrowIfLocalIpGood($ip) : void {
|
||||
$this->localAddressChecker->throwIfLocalIp($ip);
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function dataPublicIPs() : array {
|
||||
return [
|
||||
['8.8.8.8'],
|
||||
['8.8.4.4'],
|
||||
['2001:4860:4860::8888'],
|
||||
['2001:4860:4860::8844'],
|
||||
];
|
||||
}
|
||||
|
||||
public function dataInternalIPs() : array {
|
||||
return [
|
||||
['192.168.0.1'],
|
||||
['fe80::200:5aee:feaa:20a2'],
|
||||
['0:0:0:0:0:ffff:10.0.0.1'],
|
||||
['0:0:0:0:0:ffff:127.0.0.0'],
|
||||
['10.0.0.1'],
|
||||
['::'],
|
||||
['::1'],
|
||||
['100.100.100.200'],
|
||||
['192.0.0.1'],
|
||||
];
|
||||
}
|
||||
|
||||
public function dataPreventLocalAddress():array {
|
||||
return [
|
||||
['localhost/foo.bar'],
|
||||
['localHost/foo.bar'],
|
||||
['random-host/foo.bar'],
|
||||
['[::1]/bla.blub'],
|
||||
['[::]/bla.blub'],
|
||||
['192.168.0.1'],
|
||||
['172.16.42.1'],
|
||||
['[fdf8:f53b:82e4::53]/secret.ics'],
|
||||
['[fe80::200:5aee:feaa:20a2]/secret.ics'],
|
||||
['[0:0:0:0:0:ffff:10.0.0.1]/secret.ics'],
|
||||
['[0:0:0:0:0:ffff:127.0.0.0]/secret.ics'],
|
||||
['10.0.0.1'],
|
||||
['another-host.local'],
|
||||
['service.localhost'],
|
||||
['!@#$'], // test invalid url
|
||||
['100.100.100.200'],
|
||||
['192.0.0.1'],
|
||||
['randomdomain.internal'],
|
||||
['0177.0.0.9'],
|
||||
['⑯⑨。②⑤④。⑯⑨。②⑤④'],
|
||||
['127。②⑤④。⑯⑨.②⑤④'],
|
||||
['127.0.00000000000000000000000000000000001'],
|
||||
['127.1'],
|
||||
['127.000.001'],
|
||||
['0177.0.0.01'],
|
||||
['0x7f.0x0.0x0.0x1'],
|
||||
['0x7f000001'],
|
||||
['2130706433'],
|
||||
['00000000000000000000000000000000000000000000000000177.1'],
|
||||
['0x7f.1'],
|
||||
['127.0x1'],
|
||||
['[0000:0000:0000:0000:0000:0000:0000:0001]'],
|
||||
['[0:0:0:0:0:0:0:1]'],
|
||||
['[0:0:0:0::0:0:1]'],
|
||||
['%31%32%37%2E%30%2E%30%2E%31'],
|
||||
['%31%32%37%2E%30%2E%30.%31'],
|
||||
['[%3A%3A%31]'],
|
||||
];
|
||||
}
|
||||
|
||||
public function dataAllowLocalAddress():array {
|
||||
return [
|
||||
['example.com/foo.bar'],
|
||||
['example.net/foo.bar'],
|
||||
['example.org/foo.bar'],
|
||||
['8.8.8.8/bla.blub'],
|
||||
['8.8.4.4/bla.blub'],
|
||||
['8.8.8.8'],
|
||||
['8.8.4.4'],
|
||||
['[2001:4860:4860::8888]/secret.ics'],
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* @copyright 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace lib\Net;
|
||||
|
||||
use OC\Net\HostnameClassifier;
|
||||
use Test\TestCase;
|
||||
|
||||
class HostnameClassifierTest extends TestCase {
|
||||
private HostnameClassifier $classifier;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->classifier = new HostnameClassifier();
|
||||
}
|
||||
|
||||
public function localHostnamesData():array {
|
||||
return [
|
||||
['localhost'],
|
||||
['localHost'],
|
||||
['random-host'],
|
||||
['another-host.local'],
|
||||
['service.localhost'],
|
||||
['randomdomain.internal'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider localHostnamesData
|
||||
*/
|
||||
public function testLocalHostname(string $host): void {
|
||||
$isLocal = $this->classifier->isLocalHostname($host);
|
||||
|
||||
self::assertTrue($isLocal);
|
||||
}
|
||||
|
||||
public function publicHostnamesData(): array {
|
||||
return [
|
||||
['example.com'],
|
||||
['example.net'],
|
||||
['example.org'],
|
||||
['host.domain'],
|
||||
['cloud.domain.tld'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider publicHostnamesData
|
||||
*/
|
||||
public function testPublicHostname(string $host): void {
|
||||
$isLocal = $this->classifier->isLocalHostname($host);
|
||||
|
||||
self::assertFalse($isLocal);
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* @copyright 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace lib\Net;
|
||||
|
||||
use OC\Net\IpAddressClassifier;
|
||||
use Test\TestCase;
|
||||
|
||||
class IpAddressClassifierTest extends TestCase {
|
||||
private IpAddressClassifier $classifier;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->classifier = new IpAddressClassifier();
|
||||
}
|
||||
|
||||
public function publicIpAddressData(): array {
|
||||
return [
|
||||
['8.8.8.8'],
|
||||
['8.8.4.4'],
|
||||
['2001:4860:4860::8888'],
|
||||
['2001:4860:4860::8844'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider publicIpAddressData
|
||||
*/
|
||||
public function testPublicAddress(string $ip): void {
|
||||
$isLocal = $this->classifier->isLocalAddress($ip);
|
||||
|
||||
self::assertFalse($isLocal);
|
||||
}
|
||||
|
||||
public function localIpAddressData(): array {
|
||||
return [
|
||||
['192.168.0.1'],
|
||||
['fe80::200:5aee:feaa:20a2'],
|
||||
['0:0:0:0:0:ffff:10.0.0.1'],
|
||||
['0:0:0:0:0:ffff:127.0.0.0'],
|
||||
['10.0.0.1'],
|
||||
['::'],
|
||||
['::1'],
|
||||
['100.100.100.200'],
|
||||
['192.0.0.1'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider localIpAddressData
|
||||
*/
|
||||
public function testLocalAddress(string $ip): void {
|
||||
$isLocal = $this->classifier->isLocalAddress($ip);
|
||||
|
||||
self::assertTrue($isLocal);
|
||||
}
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* @copyright 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace lib\Security;
|
||||
|
||||
use OC\Net\HostnameClassifier;
|
||||
use OC\Net\IpAddressClassifier;
|
||||
use OC\Security\RemoteHostValidator;
|
||||
use OCP\IConfig;
|
||||
use OCP\Server;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\NullLogger;
|
||||
use Test\TestCase;
|
||||
|
||||
class RemoteHostValidatorIntegrationTest extends TestCase {
|
||||
|
||||
/** @var IConfig|IConfig&MockObject|MockObject */
|
||||
private IConfig $config;
|
||||
private RemoteHostValidator $validator;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
// Mock config to avoid any side effects
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
|
||||
$this->validator = new RemoteHostValidator(
|
||||
$this->config,
|
||||
Server::get(HostnameClassifier::class),
|
||||
Server::get(IpAddressClassifier::class),
|
||||
new NullLogger(),
|
||||
);
|
||||
}
|
||||
|
||||
public function localHostsData(): array {
|
||||
return [
|
||||
['[::1]'],
|
||||
['[::]'],
|
||||
['192.168.0.1'],
|
||||
['172.16.42.1'],
|
||||
['[fdf8:f53b:82e4::53]'],
|
||||
['[fe80::200:5aee:feaa:20a2]'],
|
||||
['[0:0:0:0:0:ffff:10.0.0.1]'],
|
||||
['[0:0:0:0:0:ffff:127.0.0.0]'],
|
||||
['10.0.0.1'],
|
||||
['!@#$'], // test invalid url
|
||||
['100.100.100.200'],
|
||||
['192.0.0.1'],
|
||||
['0177.0.0.9'],
|
||||
['⑯⑨。②⑤④。⑯⑨。②⑤④'],
|
||||
['127。②⑤④。⑯⑨.②⑤④'],
|
||||
['127.0.00000000000000000000000000000000001'],
|
||||
['127.1'],
|
||||
['127.000.001'],
|
||||
['0177.0.0.01'],
|
||||
['0x7f.0x0.0x0.0x1'],
|
||||
['0x7f000001'],
|
||||
['2130706433'],
|
||||
['00000000000000000000000000000000000000000000000000177.1'],
|
||||
['0x7f.1'],
|
||||
['127.0x1'],
|
||||
['[0000:0000:0000:0000:0000:0000:0000:0001]'],
|
||||
['[0:0:0:0:0:0:0:1]'],
|
||||
['[0:0:0:0::0:0:1]'],
|
||||
['%31%32%37%2E%30%2E%30%2E%31'],
|
||||
['%31%32%37%2E%30%2E%30.%31'],
|
||||
['[%3A%3A%31]'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider localHostsData
|
||||
*/
|
||||
public function testLocalHostsWhenNotAllowed(string $host): void {
|
||||
$this->config
|
||||
->method('getSystemValueBool')
|
||||
->with('allow_local_remote_servers', false)
|
||||
->willReturn(false);
|
||||
|
||||
$isValid = $this->validator->isValid($host);
|
||||
|
||||
self::assertFalse($isValid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider localHostsData
|
||||
*/
|
||||
public function testLocalHostsWhenAllowed(string $host): void {
|
||||
$this->config
|
||||
->method('getSystemValueBool')
|
||||
->with('allow_local_remote_servers', false)
|
||||
->willReturn(true);
|
||||
|
||||
$isValid = $this->validator->isValid($host);
|
||||
|
||||
self::assertTrue($isValid);
|
||||
}
|
||||
|
||||
public function externalAddressesData():array {
|
||||
return [
|
||||
['8.8.8.8'],
|
||||
['8.8.4.4'],
|
||||
['8.8.8.8'],
|
||||
['8.8.4.4'],
|
||||
['[2001:4860:4860::8888]'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider externalAddressesData
|
||||
*/
|
||||
public function testExternalHost(string $host): void {
|
||||
$this->config
|
||||
->method('getSystemValueBool')
|
||||
->with('allow_local_remote_servers', false)
|
||||
->willReturn(false);
|
||||
|
||||
$isValid = $this->validator->isValid($host);
|
||||
|
||||
self::assertTrue($isValid);
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* @copyright 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace lib\Security;
|
||||
|
||||
use OC\Net\HostnameClassifier;
|
||||
use OC\Net\IpAddressClassifier;
|
||||
use OC\Security\RemoteHostValidator;
|
||||
use OCP\IConfig;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Test\TestCase;
|
||||
|
||||
class RemoteHostValidatorTest extends TestCase {
|
||||
|
||||
/** @var IConfig|IConfig&MockObject|MockObject */
|
||||
private IConfig $config;
|
||||
/** @var HostnameClassifier|HostnameClassifier&MockObject|MockObject */
|
||||
private HostnameClassifier $hostnameClassifier;
|
||||
/** @var IpAddressClassifier|IpAddressClassifier&MockObject|MockObject */
|
||||
private IpAddressClassifier $ipAddressClassifier;
|
||||
/** @var MockObject|LoggerInterface|LoggerInterface&MockObject */
|
||||
private LoggerInterface $logger;
|
||||
private RemoteHostValidator $validator;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->hostnameClassifier = $this->createMock(HostnameClassifier::class);
|
||||
$this->ipAddressClassifier = $this->createMock(IpAddressClassifier::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
|
||||
$this->validator = new RemoteHostValidator(
|
||||
$this->config,
|
||||
$this->hostnameClassifier,
|
||||
$this->ipAddressClassifier,
|
||||
$this->logger,
|
||||
);
|
||||
}
|
||||
|
||||
public function testValid(): void {
|
||||
$host = 'nextcloud.com';
|
||||
$this->hostnameClassifier
|
||||
->method('isLocalHostname')
|
||||
->with($host)
|
||||
->willReturn(false);
|
||||
$this->ipAddressClassifier
|
||||
->method('isLocalAddress')
|
||||
->with($host)
|
||||
->willReturn(false);
|
||||
|
||||
$valid = $this->validator->isValid($host);
|
||||
|
||||
self::assertTrue($valid);
|
||||
}
|
||||
|
||||
public function testLocalHostname(): void {
|
||||
$host = 'localhost';
|
||||
$this->hostnameClassifier
|
||||
->method('isLocalHostname')
|
||||
->with($host)
|
||||
->willReturn(true);
|
||||
$this->ipAddressClassifier
|
||||
->method('isLocalAddress')
|
||||
->with($host)
|
||||
->willReturn(false);
|
||||
|
||||
$valid = $this->validator->isValid($host);
|
||||
|
||||
self::assertFalse($valid);
|
||||
}
|
||||
|
||||
public function testLocalAddress(): void {
|
||||
$host = '10.0.0.10';
|
||||
$this->hostnameClassifier
|
||||
->method('isLocalHostname')
|
||||
->with($host)
|
||||
->willReturn(false);
|
||||
$this->ipAddressClassifier
|
||||
->method('isLocalAddress')
|
||||
->with($host)
|
||||
->willReturn(true);
|
||||
|
||||
$valid = $this->validator->isValid($host);
|
||||
|
||||
self::assertFalse($valid);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue