aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Base.php3
-rw-r--r--apps/dav/lib/CalDAV/CalDavBackend.php21
-rw-r--r--apps/dav/lib/CalDAV/Schedule/IMipPlugin.php9
-rw-r--r--apps/dav/lib/CardDAV/CardDavBackend.php16
-rw-r--r--apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php7
-rw-r--r--build/psalm-baseline.xml11
-rw-r--r--lib/base.php17
-rw-r--r--lib/composer/composer/autoload_classmap.php5
-rw-r--r--lib/composer/composer/autoload_static.php5
-rw-r--r--lib/private/Http/Client/Client.php18
-rw-r--r--lib/private/Http/Client/ClientService.php10
-rw-r--r--lib/private/Http/Client/DnsPinMiddleware.php14
-rw-r--r--lib/private/Http/Client/LocalAddressChecker.php102
-rw-r--r--lib/private/Log/ErrorHandler.php84
-rw-r--r--lib/private/Net/HostnameClassifier.php74
-rw-r--r--lib/private/Net/IpAddressClassifier.php81
-rw-r--r--lib/private/Security/RemoteHostValidator.php76
-rw-r--r--lib/private/Server.php15
-rw-r--r--lib/public/Security/IRemoteHostValidator.php51
-rw-r--r--tests/lib/Contacts/ContactsMenu/ContactsStoreTest.php1
-rw-r--r--tests/lib/ErrorHandlerTest.php49
-rw-r--r--tests/lib/Http/Client/ClientServiceTest.php11
-rw-r--r--tests/lib/Http/Client/ClientTest.php114
-rw-r--r--tests/lib/Http/Client/LocalAddressCheckerTest.php158
-rw-r--r--tests/lib/Net/HostnameClassifierTest.php78
-rw-r--r--tests/lib/Net/IpAddressClassifierTest.php80
-rw-r--r--tests/lib/Security/RemoteHostValidatorIntegrationTest.php144
-rw-r--r--tests/lib/Security/RemoteHostValidatorTest.php111
28 files changed, 890 insertions, 475 deletions
diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Base.php b/apps/dav/lib/CalDAV/Activity/Provider/Base.php
index 48ed7b8b107..672129a8311 100644
--- a/apps/dav/lib/CalDAV/Activity/Provider/Base.php
+++ b/apps/dav/lib/CalDAV/Activity/Provider/Base.php
@@ -38,9 +38,6 @@ abstract class Base implements IProvider {
/** @var IUserManager */
protected $userManager;
- /** @var string[] */
- protected $userDisplayNames = [];
-
/** @var IGroupManager */
protected $groupManager;
diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php
index b00a4f9d6ed..a894b65d756 100644
--- a/apps/dav/lib/CalDAV/CalDavBackend.php
+++ b/apps/dav/lib/CalDAV/CalDavBackend.php
@@ -417,7 +417,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
[, $name] = Uri\split($row['principaluri']);
$uri = $row['uri'] . '_shared_by_' . $name;
- $row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
+ $row['displayname'] = $row['displayname'] . ' (' . ($this->userManager->getDisplayName($name) ?? ($name ?? '')) . ')';
$components = [];
if ($row['components']) {
$components = explode(',',$row['components']);
@@ -493,25 +493,6 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
return array_values($calendars);
}
-
- /**
- * @param $uid
- * @return string
- */
- private function getUserDisplayName($uid) {
- if (!isset($this->userDisplayNames[$uid])) {
- $user = $this->userManager->get($uid);
-
- if ($user instanceof IUser) {
- $this->userDisplayNames[$uid] = $user->getDisplayName();
- } else {
- $this->userDisplayNames[$uid] = $uid;
- }
- }
-
- return $this->userDisplayNames[$uid];
- }
-
/**
* @return array
*/
diff --git a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
index 2c7b06a4396..515072fd227 100644
--- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
+++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
@@ -178,12 +178,7 @@ class IMipPlugin extends SabreIMipPlugin {
$recipientName = $iTipMessage->recipientName ?: null;
if ($senderName === null || empty(trim($senderName))) {
- $user = $this->userManager->get($this->userId);
- if ($user) {
- // getDisplayName automatically uses the uid
- // if no display-name is set
- $senderName = $user->getDisplayName();
- }
+ $senderName = $this->userManager->getDisplayName($this->userId);
}
/** @var VEvent $vevent */
@@ -225,7 +220,7 @@ class IMipPlugin extends SabreIMipPlugin {
];
$fromEMail = Util::getDefaultEmailAddress('invitations-noreply');
- $fromName = $l10n->t('%1$s via %2$s', [$senderName, $this->defaults->getName()]);
+ $fromName = $l10n->t('%1$s via %2$s', [$senderName ?? $this->userId, $this->defaults->getName()]);
$message = $this->mailer->createMessage()
->setFrom([$fromEMail => $fromName])
diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php
index 72499ad3037..03ddb93c084 100644
--- a/apps/dav/lib/CardDAV/CardDavBackend.php
+++ b/apps/dav/lib/CardDAV/CardDavBackend.php
@@ -207,7 +207,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
[, $name] = \Sabre\Uri\split($row['principaluri']);
$uri = $row['uri'] . '_shared_by_' . $name;
- $displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
+ $displayName = $row['displayname'] . ' (' . ($this->userManager->getDisplayName($name) ?? $name ?? '') . ')';
$addressBooks[$row['id']] = [
'id' => $row['id'],
@@ -256,20 +256,6 @@ class CardDavBackend implements BackendInterface, SyncSupport {
return array_values($addressBooks);
}
- private function getUserDisplayName($uid) {
- if (!isset($this->userDisplayNames[$uid])) {
- $user = $this->userManager->get($uid);
-
- if ($user instanceof IUser) {
- $this->userDisplayNames[$uid] = $user->getDisplayName();
- } else {
- $this->userDisplayNames[$uid] = $uid;
- }
- }
-
- return $this->userDisplayNames[$uid];
- }
-
/**
* @param int $addressBookId
*/
diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php
index 0d8076f7aa4..0b64704a87e 100644
--- a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php
+++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php
@@ -183,13 +183,10 @@ class IMipPluginTest extends TestCase {
$message = $this->_testMessage();
$message->senderName = null;
- $user = $this->createMock(IUser::class);
- $user->method('getDisplayName')->willReturn('Mr. Wizard');
-
$this->userManager->expects($this->once())
- ->method('get')
+ ->method('getDisplayName')
->with('user123')
- ->willReturn($user);
+ ->willReturn('Mr. Wizard');
$this->_expectSend();
$this->plugin->schedule($message);
diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml
index 7ce93ebff71..9e50ec46011 100644
--- a/build/psalm-baseline.xml
+++ b/build/psalm-baseline.xml
@@ -2777,19 +2777,12 @@
<InvalidOperand occurrences="1">
<code>$result</code>
</InvalidOperand>
- <InvalidReturnStatement occurrences="3">
- <code>$helper-&gt;getFileSize($fullPath)</code>
+ <InvalidReturnStatement occurrences="1">
<code>$result</code>
- <code>$space</code>
</InvalidReturnStatement>
- <InvalidReturnType occurrences="3">
- <code>filesize</code>
- <code>free_space</code>
+ <InvalidReturnType occurrences="1">
<code>rename</code>
</InvalidReturnType>
- <NullableReturnStatement occurrences="1">
- <code>$helper-&gt;getFileSize($fullPath)</code>
- </NullableReturnStatement>
<TypeDoesNotContainNull occurrences="2">
<code>$space === false || is_null($space)</code>
<code>is_null($space)</code>
diff --git a/lib/base.php b/lib/base.php
index 04c456a12d8..fa69a7d6076 100644
--- a/lib/base.php
+++ b/lib/base.php
@@ -662,9 +662,20 @@ class OC {
$config = \OC::$server->get(\OCP\IConfig::class);
if (!defined('PHPUNIT_RUN')) {
- OC\Log\ErrorHandler::setLogger(\OC::$server->getLogger());
- $debug = $config->getSystemValue('debug', false);
- OC\Log\ErrorHandler::register($debug);
+ $errorHandler = new OC\Log\ErrorHandler(
+ \OCP\Server::get(\Psr\Log\LoggerInterface::class),
+ );
+ $exceptionHandler = [$errorHandler, 'onException'];
+ if ($config->getSystemValue('debug', false)) {
+ set_error_handler([$errorHandler, 'onAll'], E_ALL);
+ if (\OC::$CLI) {
+ $exceptionHandler = ['OC_Template', 'printExceptionErrorPage'];
+ }
+ } else {
+ set_error_handler([$errorHandler, 'onError']);
+ }
+ register_shutdown_function([$errorHandler, 'onShutdown']);
+ set_exception_handler($exceptionHandler);
}
/** @var \OC\AppFramework\Bootstrap\Coordinator $bootstrapCoordinator */
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 2b5475f6efb..397f36e660b 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -535,6 +535,7 @@ return array(
'OCP\\Security\\ICredentialsManager' => $baseDir . '/lib/public/Security/ICredentialsManager.php',
'OCP\\Security\\ICrypto' => $baseDir . '/lib/public/Security/ICrypto.php',
'OCP\\Security\\IHasher' => $baseDir . '/lib/public/Security/IHasher.php',
+ 'OCP\\Security\\IRemoteHostValidator' => $baseDir . '/lib/public/Security/IRemoteHostValidator.php',
'OCP\\Security\\ISecureRandom' => $baseDir . '/lib/public/Security/ISecureRandom.php',
'OCP\\Security\\ITrustedDomainHelper' => $baseDir . '/lib/public/Security/ITrustedDomainHelper.php',
'OCP\\Security\\VerificationToken\\IVerificationToken' => $baseDir . '/lib/public/Security/VerificationToken/IVerificationToken.php',
@@ -1288,7 +1289,6 @@ return array(
'OC\\Http\\Client\\Client' => $baseDir . '/lib/private/Http/Client/Client.php',
'OC\\Http\\Client\\ClientService' => $baseDir . '/lib/private/Http/Client/ClientService.php',
'OC\\Http\\Client\\DnsPinMiddleware' => $baseDir . '/lib/private/Http/Client/DnsPinMiddleware.php',
- 'OC\\Http\\Client\\LocalAddressChecker' => $baseDir . '/lib/private/Http/Client/LocalAddressChecker.php',
'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',
@@ -1362,6 +1362,8 @@ return array(
'OC\\NaturalSort_DefaultCollator' => $baseDir . '/lib/private/NaturalSort_DefaultCollator.php',
'OC\\NavigationManager' => $baseDir . '/lib/private/NavigationManager.php',
'OC\\NeedsUpdateException' => $baseDir . '/lib/private/NeedsUpdateException.php',
+ 'OC\\Net\\HostnameClassifier' => $baseDir . '/lib/private/Net/HostnameClassifier.php',
+ 'OC\\Net\\IpAddressClassifier' => $baseDir . '/lib/private/Net/IpAddressClassifier.php',
'OC\\NotSquareException' => $baseDir . '/lib/private/NotSquareException.php',
'OC\\Notification\\Action' => $baseDir . '/lib/private/Notification/Action.php',
'OC\\Notification\\Manager' => $baseDir . '/lib/private/Notification/Manager.php',
@@ -1517,6 +1519,7 @@ return array(
'OC\\Security\\RateLimiting\\Backend\\MemoryCacheBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php',
'OC\\Security\\RateLimiting\\Exception\\RateLimitExceededException' => $baseDir . '/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php',
'OC\\Security\\RateLimiting\\Limiter' => $baseDir . '/lib/private/Security/RateLimiting/Limiter.php',
+ 'OC\\Security\\RemoteHostValidator' => $baseDir . '/lib/private/Security/RemoteHostValidator.php',
'OC\\Security\\SecureRandom' => $baseDir . '/lib/private/Security/SecureRandom.php',
'OC\\Security\\TrustedDomainHelper' => $baseDir . '/lib/private/Security/TrustedDomainHelper.php',
'OC\\Security\\VerificationToken\\CleanUpJob' => $baseDir . '/lib/private/Security/VerificationToken/CleanUpJob.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index ad62c3585e6..4c430720ef1 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -568,6 +568,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Security\\ICredentialsManager' => __DIR__ . '/../../..' . '/lib/public/Security/ICredentialsManager.php',
'OCP\\Security\\ICrypto' => __DIR__ . '/../../..' . '/lib/public/Security/ICrypto.php',
'OCP\\Security\\IHasher' => __DIR__ . '/../../..' . '/lib/public/Security/IHasher.php',
+ 'OCP\\Security\\IRemoteHostValidator' => __DIR__ . '/../../..' . '/lib/public/Security/IRemoteHostValidator.php',
'OCP\\Security\\ISecureRandom' => __DIR__ . '/../../..' . '/lib/public/Security/ISecureRandom.php',
'OCP\\Security\\ITrustedDomainHelper' => __DIR__ . '/../../..' . '/lib/public/Security/ITrustedDomainHelper.php',
'OCP\\Security\\VerificationToken\\IVerificationToken' => __DIR__ . '/../../..' . '/lib/public/Security/VerificationToken/IVerificationToken.php',
@@ -1321,7 +1322,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Http\\Client\\Client' => __DIR__ . '/../../..' . '/lib/private/Http/Client/Client.php',
'OC\\Http\\Client\\ClientService' => __DIR__ . '/../../..' . '/lib/private/Http/Client/ClientService.php',
'OC\\Http\\Client\\DnsPinMiddleware' => __DIR__ . '/../../..' . '/lib/private/Http/Client/DnsPinMiddleware.php',
- 'OC\\Http\\Client\\LocalAddressChecker' => __DIR__ . '/../../..' . '/lib/private/Http/Client/LocalAddressChecker.php',
'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',
@@ -1395,6 +1395,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\NaturalSort_DefaultCollator' => __DIR__ . '/../../..' . '/lib/private/NaturalSort_DefaultCollator.php',
'OC\\NavigationManager' => __DIR__ . '/../../..' . '/lib/private/NavigationManager.php',
'OC\\NeedsUpdateException' => __DIR__ . '/../../..' . '/lib/private/NeedsUpdateException.php',
+ 'OC\\Net\\HostnameClassifier' => __DIR__ . '/../../..' . '/lib/private/Net/HostnameClassifier.php',
+ 'OC\\Net\\IpAddressClassifier' => __DIR__ . '/../../..' . '/lib/private/Net/IpAddressClassifier.php',
'OC\\NotSquareException' => __DIR__ . '/../../..' . '/lib/private/NotSquareException.php',
'OC\\Notification\\Action' => __DIR__ . '/../../..' . '/lib/private/Notification/Action.php',
'OC\\Notification\\Manager' => __DIR__ . '/../../..' . '/lib/private/Notification/Manager.php',
@@ -1550,6 +1552,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Security\\RateLimiting\\Backend\\MemoryCacheBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php',
'OC\\Security\\RateLimiting\\Exception\\RateLimitExceededException' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php',
'OC\\Security\\RateLimiting\\Limiter' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Limiter.php',
+ 'OC\\Security\\RemoteHostValidator' => __DIR__ . '/../../..' . '/lib/private/Security/RemoteHostValidator.php',
'OC\\Security\\SecureRandom' => __DIR__ . '/../../..' . '/lib/private/Security/SecureRandom.php',
'OC\\Security\\TrustedDomainHelper' => __DIR__ . '/../../..' . '/lib/private/Security/TrustedDomainHelper.php',
'OC\\Security\\VerificationToken\\CleanUpJob' => __DIR__ . '/../../..' . '/lib/private/Security/VerificationToken/CleanUpJob.php',
diff --git a/lib/private/Http/Client/Client.php b/lib/private/Http/Client/Client.php
index d4dba3e5a44..2e370395132 100644
--- a/lib/private/Http/Client/Client.php
+++ b/lib/private/Http/Client/Client.php
@@ -37,8 +37,11 @@ use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\RequestOptions;
use OCP\Http\Client\IClient;
use OCP\Http\Client\IResponse;
+use OCP\Http\Client\LocalServerException;
use OCP\ICertificateManager;
use OCP\IConfig;
+use OCP\Security\IRemoteHostValidator;
+use function parse_url;
/**
* Class Client
@@ -52,19 +55,18 @@ class Client implements IClient {
private $config;
/** @var ICertificateManager */
private $certificateManager;
- /** @var LocalAddressChecker */
- private $localAddressChecker;
+ private IRemoteHostValidator $remoteHostValidator;
public function __construct(
IConfig $config,
ICertificateManager $certificateManager,
GuzzleClient $client,
- LocalAddressChecker $localAddressChecker
+ IRemoteHostValidator $remoteHostValidator
) {
$this->config = $config;
$this->client = $client;
$this->certificateManager = $certificateManager;
- $this->localAddressChecker = $localAddressChecker;
+ $this->remoteHostValidator = $remoteHostValidator;
}
private function buildRequestOptions(array $options): array {
@@ -181,7 +183,13 @@ class Client implements IClient {
return;
}
- $this->localAddressChecker->throwIfLocalAddress($uri);
+ $host = parse_url($uri, PHP_URL_HOST);
+ if ($host === false || $host === null) {
+ throw new LocalServerException('Could not detect any host');
+ }
+ if (!$this->remoteHostValidator->isValid($host)) {
+ throw new LocalServerException('Host violates local access rules');
+ }
}
/**
diff --git a/lib/private/Http/Client/ClientService.php b/lib/private/Http/Client/ClientService.php
index e868d4af7a5..bbc2330176f 100644
--- a/lib/private/Http/Client/ClientService.php
+++ b/lib/private/Http/Client/ClientService.php
@@ -33,6 +33,7 @@ use OCP\Http\Client\IClient;
use OCP\Http\Client\IClientService;
use OCP\ICertificateManager;
use OCP\IConfig;
+use OCP\Security\IRemoteHostValidator;
/**
* Class ClientService
@@ -46,17 +47,16 @@ class ClientService implements IClientService {
private $certificateManager;
/** @var DnsPinMiddleware */
private $dnsPinMiddleware;
- /** @var LocalAddressChecker */
- private $localAddressChecker;
+ private IRemoteHostValidator $remoteHostValidator;
public function __construct(IConfig $config,
ICertificateManager $certificateManager,
DnsPinMiddleware $dnsPinMiddleware,
- LocalAddressChecker $localAddressChecker) {
+ IRemoteHostValidator $remoteHostValidator) {
$this->config = $config;
$this->certificateManager = $certificateManager;
$this->dnsPinMiddleware = $dnsPinMiddleware;
- $this->localAddressChecker = $localAddressChecker;
+ $this->remoteHostValidator = $remoteHostValidator;
}
/**
@@ -73,7 +73,7 @@ class ClientService implements IClientService {
$this->config,
$this->certificateManager,
$client,
- $this->localAddressChecker
+ $this->remoteHostValidator,
);
}
}
diff --git a/lib/private/Http/Client/DnsPinMiddleware.php b/lib/private/Http/Client/DnsPinMiddleware.php
index 00bc209d7b1..294a23f9de1 100644
--- a/lib/private/Http/Client/DnsPinMiddleware.php
+++ b/lib/private/Http/Client/DnsPinMiddleware.php
@@ -25,20 +25,21 @@ declare(strict_types=1);
*/
namespace OC\Http\Client;
+use OC\Net\IpAddressClassifier;
+use OCP\Http\Client\LocalServerException;
use Psr\Http\Message\RequestInterface;
class DnsPinMiddleware {
/** @var NegativeDnsCache */
private $negativeDnsCache;
- /** @var LocalAddressChecker */
- private $localAddressChecker;
+ private IpAddressClassifier $ipAddressClassifier;
public function __construct(
NegativeDnsCache $negativeDnsCache,
- LocalAddressChecker $localAddressChecker
+ IpAddressClassifier $ipAddressClassifier
) {
$this->negativeDnsCache = $negativeDnsCache;
- $this->localAddressChecker = $localAddressChecker;
+ $this->ipAddressClassifier = $ipAddressClassifier;
}
/**
@@ -133,7 +134,10 @@ class DnsPinMiddleware {
$curlResolves["$hostName:$port"] = [];
foreach ($targetIps as $ip) {
- $this->localAddressChecker->throwIfLocalIp($ip);
+ if (!$this->ipAddressClassifier->isLocalAddress($ip)) {
+ // TODO: continue with all non-local IPs?
+ throw new LocalServerException('Host violates local access rules');
+ }
$curlResolves["$hostName:$port"][] = $ip;
}
}
diff --git a/lib/private/Http/Client/LocalAddressChecker.php b/lib/private/Http/Client/LocalAddressChecker.php
deleted file mode 100644
index eb24f002d7d..00000000000
--- a/lib/private/Http/Client/LocalAddressChecker.php
+++ /dev/null
@@ -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);
- }
-}
diff --git a/lib/private/Log/ErrorHandler.php b/lib/private/Log/ErrorHandler.php
index d56fecb1ecb..c4b9631e75a 100644
--- a/lib/private/Log/ErrorHandler.php
+++ b/lib/private/Log/ErrorHandler.php
@@ -1,4 +1,7 @@
<?php
+
+declare(strict_types=1);
+
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
@@ -27,84 +30,75 @@
*/
namespace OC\Log;
+use Error;
use OCP\ILogger;
+use Psr\Log\LoggerInterface;
+use Throwable;
class ErrorHandler {
- /** @var ILogger */
- private static $logger;
+ private LoggerInterface $logger;
+
+ public function __construct(LoggerInterface $logger) {
+ $this->logger = $logger;
+ }
/**
- * remove password in URLs
- * @param string $msg
- * @return string
+ * Remove password in URLs
*/
- protected static function removePassword($msg) {
+ private static function removePassword(string $msg): string {
return preg_replace('#//(.*):(.*)@#', '//xxx:xxx@', $msg);
}
- public static function register($debug = false) {
- $handler = new ErrorHandler();
-
- if ($debug) {
- set_error_handler([$handler, 'onAll'], E_ALL);
- if (\OC::$CLI) {
- set_exception_handler(['OC_Template', 'printExceptionErrorPage']);
- }
- } else {
- set_error_handler([$handler, 'onError']);
- }
- register_shutdown_function([$handler, 'onShutdown']);
- set_exception_handler([$handler, 'onException']);
- }
-
- public static function setLogger(ILogger $logger) {
- self::$logger = $logger;
- }
-
- //Fatal errors handler
- public static function onShutdown() {
+ /**
+ * Fatal errors handler
+ */
+ public function onShutdown(): void {
$error = error_get_last();
- if ($error && self::$logger) {
- //ob_end_clean();
+ if ($error) {
$msg = $error['message'] . ' at ' . $error['file'] . '#' . $error['line'];
- self::$logger->critical(self::removePassword($msg), ['app' => 'PHP']);
+ $this->logger->critical(self::removePassword($msg), ['app' => 'PHP']);
}
}
/**
- * Uncaught exception handler
- *
- * @param \Exception $exception
+ * Uncaught exception handler
*/
- public static function onException($exception) {
+ public function onException(Throwable $exception): void {
$class = get_class($exception);
$msg = $exception->getMessage();
$msg = "$class: $msg at " . $exception->getFile() . '#' . $exception->getLine();
- self::$logger->critical(self::removePassword($msg), ['app' => 'PHP']);
+ $this->logger->critical(self::removePassword($msg), ['app' => 'PHP']);
}
- //Recoverable errors handler
- public static function onError($number, $message, $file, $line) {
+ /**
+ * Recoverable errors handler
+ */
+ public function onError(int $number, string $message, string $file, int $line): bool {
if (!(error_reporting() & $number)) {
- return;
+ return true;
}
$msg = $message . ' at ' . $file . '#' . $line;
- $e = new \Error(self::removePassword($msg));
- self::$logger->logException($e, ['app' => 'PHP', 'level' => self::errnoToLogLevel($number)]);
+ $e = new Error(self::removePassword($msg));
+ $this->logger->log(self::errnoToLogLevel($number), $e->getMessage(), ['app' => 'PHP']);
+ return true;
}
- //Recoverable handler which catch all errors, warnings and notices
- public static function onAll($number, $message, $file, $line) {
+ /**
+ * Recoverable handler which catch all errors, warnings and notices
+ */
+ public function onAll(int $number, string $message, string $file, int $line): bool {
$msg = $message . ' at ' . $file . '#' . $line;
- $e = new \Error(self::removePassword($msg));
- self::$logger->logException($e, ['app' => 'PHP', 'level' => self::errnoToLogLevel($number)]);
+ $e = new Error(self::removePassword($msg));
+ $this->logger->log(self::errnoToLogLevel($number), $e->getMessage(), ['app' => 'PHP']);
+ return true;
}
- public static function errnoToLogLevel(int $errno): int {
+ private static function errnoToLogLevel(int $errno): int {
switch ($errno) {
case E_USER_WARNING:
return ILogger::WARN;
+ case E_DEPRECATED:
case E_USER_DEPRECATED:
return ILogger::DEBUG;
diff --git a/lib/private/Net/HostnameClassifier.php b/lib/private/Net/HostnameClassifier.php
new file mode 100644
index 00000000000..626aa47083e
--- /dev/null
+++ b/lib/private/Net/HostnameClassifier.php
@@ -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;
+ }
+}
diff --git a/lib/private/Net/IpAddressClassifier.php b/lib/private/Net/IpAddressClassifier.php
new file mode 100644
index 00000000000..d4698864ec8
--- /dev/null
+++ b/lib/private/Net/IpAddressClassifier.php
@@ -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;
+ }
+}
diff --git a/lib/private/Security/RemoteHostValidator.php b/lib/private/Security/RemoteHostValidator.php
new file mode 100644
index 00000000000..e48bd862472
--- /dev/null
+++ b/lib/private/Security/RemoteHostValidator.php
@@ -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;
+ }
+}
diff --git a/lib/private/Server.php b/lib/private/Server.php
index 1da1b614b5b..03aa75060df 100644
--- a/lib/private/Server.php
+++ b/lib/private/Server.php
@@ -105,8 +105,6 @@ use OC\Files\Type\Loader;
use OC\Files\View;
use OC\FullTextSearch\FullTextSearchManager;
use OC\Http\Client\ClientService;
-use OC\Http\Client\DnsPinMiddleware;
-use OC\Http\Client\LocalAddressChecker;
use OC\Http\Client\NegativeDnsCache;
use OC\IntegrityCheck\Checker;
use OC\IntegrityCheck\Helpers\AppLocator;
@@ -858,7 +856,7 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerAlias(\OCP\Security\ISecureRandom::class, SecureRandom::class);
/** @deprecated 19.0.0 */
$this->registerDeprecatedAlias('SecureRandom', \OCP\Security\ISecureRandom::class);
-
+ $this->registerAlias(\OCP\Security\IRemoteHostValidator::class, \OC\Security\RemoteHostValidator::class);
$this->registerAlias(IVerificationToken::class, VerificationToken::class);
$this->registerAlias(ICrypto::class, Crypto::class);
@@ -890,22 +888,11 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerAlias(ICertificateManager::class, CertificateManager::class);
$this->registerAlias(IClientService::class, ClientService::class);
- $this->registerService(LocalAddressChecker::class, function (ContainerInterface $c) {
- return new LocalAddressChecker(
- $c->get(LoggerInterface::class),
- );
- });
$this->registerService(NegativeDnsCache::class, function (ContainerInterface $c) {
return new NegativeDnsCache(
$c->get(ICacheFactory::class),
);
});
- $this->registerService(DnsPinMiddleware::class, function (ContainerInterface $c) {
- return new DnsPinMiddleware(
- $c->get(NegativeDnsCache::class),
- $c->get(LocalAddressChecker::class)
- );
- });
$this->registerDeprecatedAlias('HttpClientService', IClientService::class);
$this->registerService(IEventLogger::class, function (ContainerInterface $c) {
return new EventLogger($c->get(SystemConfig::class), $c->get(LoggerInterface::class), $c->get(Log::class));
diff --git a/lib/public/Security/IRemoteHostValidator.php b/lib/public/Security/IRemoteHostValidator.php
new file mode 100644
index 00000000000..99f149aee04
--- /dev/null
+++ b/lib/public/Security/IRemoteHostValidator.php
@@ -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;
+}
diff --git a/tests/lib/Contacts/ContactsMenu/ContactsStoreTest.php b/tests/lib/Contacts/ContactsMenu/ContactsStoreTest.php
index dfdd67fbb23..a038058069e 100644
--- a/tests/lib/Contacts/ContactsMenu/ContactsStoreTest.php
+++ b/tests/lib/Contacts/ContactsMenu/ContactsStoreTest.php
@@ -863,6 +863,7 @@ class ContactsStoreTest extends TestCase {
['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
['core', 'shareapi_restrict_user_enumeration_full_match', 'yes', 'yes'],
['core', 'shareapi_exclude_groups', 'no', 'yes'],
+ ['core', 'shareapi_exclude_groups_list', '', ''],
['core', 'shareapi_only_share_with_group_members', 'no', 'no'],
]);
diff --git a/tests/lib/ErrorHandlerTest.php b/tests/lib/ErrorHandlerTest.php
index ea53e67005c..f6bf850f0b5 100644
--- a/tests/lib/ErrorHandlerTest.php
+++ b/tests/lib/ErrorHandlerTest.php
@@ -1,4 +1,7 @@
<?php
+
+declare(strict_types=1);
+
/**
* ownCloud
*
@@ -22,7 +25,26 @@
namespace Test;
-class ErrorHandlerTest extends \Test\TestCase {
+use OC\Log\ErrorHandler;
+use OCP\ILogger;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+
+class ErrorHandlerTest extends TestCase {
+
+ /** @var MockObject */
+ private LoggerInterface $logger;
+
+ private ErrorHandler $errorHandler;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->errorHandler = new ErrorHandler(
+ $this->logger
+ );
+ }
/**
* provide username, password combinations for testRemovePassword
@@ -47,24 +69,19 @@ class ErrorHandlerTest extends \Test\TestCase {
* @param string $username
* @param string $password
*/
- public function testRemovePassword($username, $password) {
+ public function testRemovePasswordFromError($username, $password) {
$url = 'http://'.$username.':'.$password.'@owncloud.org';
$expectedResult = 'http://xxx:xxx@owncloud.org';
- $result = TestableErrorHandler::testRemovePassword($url);
+ $this->logger->expects(self::once())
+ ->method('log')
+ ->with(
+ ILogger::ERROR,
+ 'Could not reach ' . $expectedResult . ' at file#4',
+ ['app' => 'PHP'],
+ );
- $this->assertEquals($expectedResult, $result);
- }
-}
+ $result = $this->errorHandler->onError(E_USER_ERROR, 'Could not reach ' . $url, 'file', 4);
-/**
- * dummy class to access protected methods of \OC\Log\ErrorHandler
- */
-class TestableErrorHandler extends \OC\Log\ErrorHandler {
-
- /**
- * @param string $msg
- */
- public static function testRemovePassword($msg) {
- return self::removePassword($msg);
+ self::assertTrue($result);
}
}
diff --git a/tests/lib/Http/Client/ClientServiceTest.php b/tests/lib/Http/Client/ClientServiceTest.php
index 94f4d51ecee..ed1165236aa 100644
--- a/tests/lib/Http/Client/ClientServiceTest.php
+++ b/tests/lib/Http/Client/ClientServiceTest.php
@@ -1,4 +1,7 @@
<?php
+
+declare(strict_types=1);
+
/**
* Copyright (c) 2015 Lukas Reschke <lukas@owncloud.com>
* This file is licensed under the Affero General Public License version 3 or
@@ -14,9 +17,9 @@ use GuzzleHttp\Handler\CurlHandler;
use OC\Http\Client\Client;
use OC\Http\Client\ClientService;
use OC\Http\Client\DnsPinMiddleware;
-use OC\Http\Client\LocalAddressChecker;
use OCP\ICertificateManager;
use OCP\IConfig;
+use OCP\Security\IRemoteHostValidator;
/**
* Class ClientServiceTest
@@ -33,13 +36,13 @@ class ClientServiceTest extends \Test\TestCase {
->method('addDnsPinning')
->willReturn(function () {
});
- $localAddressChecker = $this->createMock(LocalAddressChecker::class);
+ $remoteHostValidator = $this->createMock(IRemoteHostValidator::class);
$clientService = new ClientService(
$config,
$certificateManager,
$dnsPinMiddleware,
- $localAddressChecker
+ $remoteHostValidator
);
$handler = new CurlHandler();
@@ -52,7 +55,7 @@ class ClientServiceTest extends \Test\TestCase {
$config,
$certificateManager,
$guzzleClient,
- $localAddressChecker
+ $remoteHostValidator
),
$clientService->newClient()
);
diff --git a/tests/lib/Http/Client/ClientTest.php b/tests/lib/Http/Client/ClientTest.php
index fa2374aeb7e..93948a5daf3 100644
--- a/tests/lib/Http/Client/ClientTest.php
+++ b/tests/lib/Http/Client/ClientTest.php
@@ -1,4 +1,7 @@
<?php
+
+declare(strict_types=1);
+
/**
* Copyright (c) 2015 Lukas Reschke <lukas@owncloud.com>
* This file is licensed under the Affero General Public License version 3 or
@@ -10,12 +13,13 @@ namespace Test\Http\Client;
use GuzzleHttp\Psr7\Response;
use OC\Http\Client\Client;
-use OC\Http\Client\LocalAddressChecker;
use OC\Security\CertificateManager;
use OCP\Http\Client\LocalServerException;
use OCP\ICertificateManager;
use OCP\IConfig;
+use OCP\Security\IRemoteHostValidator;
use PHPUnit\Framework\MockObject\MockObject;
+use function parse_url;
/**
* Class ClientTest
@@ -29,8 +33,8 @@ class ClientTest extends \Test\TestCase {
private $client;
/** @var IConfig|MockObject */
private $config;
- /** @var LocalAddressChecker|MockObject */
- private $localAddressChecker;
+ /** @var IRemoteHostValidator|MockObject */
+ private IRemoteHostValidator $remoteHostValidator;
/** @var array */
private $defaultRequestOptions;
@@ -39,12 +43,12 @@ class ClientTest extends \Test\TestCase {
$this->config = $this->createMock(IConfig::class);
$this->guzzleClient = $this->createMock(\GuzzleHttp\Client::class);
$this->certificateManager = $this->createMock(ICertificateManager::class);
- $this->localAddressChecker = $this->createMock(LocalAddressChecker::class);
+ $this->remoteHostValidator = $this->createMock(IRemoteHostValidator::class);
$this->client = new Client(
$this->config,
$this->certificateManager,
$this->guzzleClient,
- $this->localAddressChecker
+ $this->remoteHostValidator
);
}
@@ -146,22 +150,22 @@ class ClientTest extends \Test\TestCase {
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:0: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
- ['normal.host.com'],
+ ['https://localhost/foo.bar'],
+ ['https://localHost/foo.bar'],
+ ['https://random-host/foo.bar'],
+ ['https://[::1]/bla.blub'],
+ ['https://[::]/bla.blub'],
+ ['https://192.168.0.1'],
+ ['https://172.16.42.1'],
+ ['https://[fdf8:f53b:82e4::53]/secret.ics'],
+ ['https://[fe80::200:5aee:feaa:20a2]/secret.ics'],
+ ['https://[0:0:0:0:0:0:10.0.0.1]/secret.ics'],
+ ['https://[0:0:0:0:0:ffff:127.0.0.0]/secret.ics'],
+ ['https://10.0.0.1'],
+ ['https://another-host.local'],
+ ['https://service.localhost'],
+ ['!@#$', true], // test invalid url
+ ['https://normal.host.com'],
];
}
@@ -175,9 +179,7 @@ class ClientTest extends \Test\TestCase {
->with('allow_local_remote_servers', false)
->willReturn(true);
-// $this->expectException(LocalServerException::class);
-
- self::invokePrivate($this->client, 'preventLocalAddress', ['http://' . $uri, []]);
+ self::invokePrivate($this->client, 'preventLocalAddress', [$uri, []]);
}
/**
@@ -188,9 +190,7 @@ class ClientTest extends \Test\TestCase {
$this->config->expects($this->never())
->method('getSystemValueBool');
-// $this->expectException(LocalServerException::class);
-
- self::invokePrivate($this->client, 'preventLocalAddress', ['http://' . $uri, [
+ self::invokePrivate($this->client, 'preventLocalAddress', [$uri, [
'nextcloud' => ['allow_local_address' => true],
]]);
}
@@ -200,14 +200,14 @@ class ClientTest extends \Test\TestCase {
* @param string $uri
*/
public function testPreventLocalAddressOnGet(string $uri): void {
+ $host = parse_url($uri, PHP_URL_HOST);
$this->expectException(LocalServerException::class);
- $this->localAddressChecker
- ->expects($this->once())
- ->method('throwIfLocalAddress')
- ->with('http://' . $uri)
- ->will($this->throwException(new LocalServerException()));
+ $this->remoteHostValidator
+ ->method('isValid')
+ ->with($host)
+ ->willReturn(false);
- $this->client->get('http://' . $uri);
+ $this->client->get($uri);
}
/**
@@ -215,14 +215,14 @@ class ClientTest extends \Test\TestCase {
* @param string $uri
*/
public function testPreventLocalAddressOnHead(string $uri): void {
+ $host = parse_url($uri, PHP_URL_HOST);
$this->expectException(LocalServerException::class);
- $this->localAddressChecker
- ->expects($this->once())
- ->method('throwIfLocalAddress')
- ->with('http://' . $uri)
- ->will($this->throwException(new LocalServerException()));
+ $this->remoteHostValidator
+ ->method('isValid')
+ ->with($host)
+ ->willReturn(false);
- $this->client->head('http://' . $uri);
+ $this->client->head($uri);
}
/**
@@ -230,14 +230,14 @@ class ClientTest extends \Test\TestCase {
* @param string $uri
*/
public function testPreventLocalAddressOnPost(string $uri): void {
+ $host = parse_url($uri, PHP_URL_HOST);
$this->expectException(LocalServerException::class);
- $this->localAddressChecker
- ->expects($this->once())
- ->method('throwIfLocalAddress')
- ->with('http://' . $uri)
- ->will($this->throwException(new LocalServerException()));
+ $this->remoteHostValidator
+ ->method('isValid')
+ ->with($host)
+ ->willReturn(false);
- $this->client->post('http://' . $uri);
+ $this->client->post($uri);
}
/**
@@ -245,14 +245,14 @@ class ClientTest extends \Test\TestCase {
* @param string $uri
*/
public function testPreventLocalAddressOnPut(string $uri): void {
+ $host = parse_url($uri, PHP_URL_HOST);
$this->expectException(LocalServerException::class);
- $this->localAddressChecker
- ->expects($this->once())
- ->method('throwIfLocalAddress')
- ->with('http://' . $uri)
- ->will($this->throwException(new LocalServerException()));
+ $this->remoteHostValidator
+ ->method('isValid')
+ ->with($host)
+ ->willReturn(false);
- $this->client->put('http://' . $uri);
+ $this->client->put($uri);
}
/**
@@ -260,14 +260,14 @@ class ClientTest extends \Test\TestCase {
* @param string $uri
*/
public function testPreventLocalAddressOnDelete(string $uri): void {
+ $host = parse_url($uri, PHP_URL_HOST);
$this->expectException(LocalServerException::class);
- $this->localAddressChecker
- ->expects($this->once())
- ->method('throwIfLocalAddress')
- ->with('http://' . $uri)
- ->will($this->throwException(new LocalServerException()));
+ $this->remoteHostValidator
+ ->method('isValid')
+ ->with($host)
+ ->willReturn(false);
- $this->client->delete('http://' . $uri);
+ $this->client->delete($uri);
}
private function setUpDefaultRequestOptions(): void {
diff --git a/tests/lib/Http/Client/LocalAddressCheckerTest.php b/tests/lib/Http/Client/LocalAddressCheckerTest.php
deleted file mode 100644
index 024c52b3705..00000000000
--- a/tests/lib/Http/Client/LocalAddressCheckerTest.php
+++ /dev/null
@@ -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'],
- ];
- }
-}
diff --git a/tests/lib/Net/HostnameClassifierTest.php b/tests/lib/Net/HostnameClassifierTest.php
new file mode 100644
index 00000000000..f363a08bb8a
--- /dev/null
+++ b/tests/lib/Net/HostnameClassifierTest.php
@@ -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);
+ }
+}
diff --git a/tests/lib/Net/IpAddressClassifierTest.php b/tests/lib/Net/IpAddressClassifierTest.php
new file mode 100644
index 00000000000..593abcd2b40
--- /dev/null
+++ b/tests/lib/Net/IpAddressClassifierTest.php
@@ -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);
+ }
+}
diff --git a/tests/lib/Security/RemoteHostValidatorIntegrationTest.php b/tests/lib/Security/RemoteHostValidatorIntegrationTest.php
new file mode 100644
index 00000000000..73cbbd7b0e8
--- /dev/null
+++ b/tests/lib/Security/RemoteHostValidatorIntegrationTest.php
@@ -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);
+ }
+}
diff --git a/tests/lib/Security/RemoteHostValidatorTest.php b/tests/lib/Security/RemoteHostValidatorTest.php
new file mode 100644
index 00000000000..acaa7a4be30
--- /dev/null
+++ b/tests/lib/Security/RemoteHostValidatorTest.php
@@ -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);
+ }
+}