Skip to content

Commit

Permalink
feat(security): Add public API to allow validating IP Ranges and chec…
Browse files Browse the repository at this point in the history
…king for "in range"

Signed-off-by: Joas Schilling <coding@schilljs.com>
Signed-off-by: Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
  • Loading branch information
nickvergessen authored and Altahrim committed Jul 19, 2024
1 parent 1f9a8f5 commit 17a6845
Show file tree
Hide file tree
Showing 17 changed files with 311 additions and 110 deletions.
25 changes: 11 additions & 14 deletions apps/settings/lib/SetupChecks/AllowedAdminRanges.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
*/
namespace OCA\Settings\SetupChecks;

use IPLib\Factory;
use OC\Security\RemoteIpAddress;
use OC\Security\Ip\Range;
use OC\Security\Ip\RemoteAddress;
use OCP\IConfig;
use OCP\IL10N;
use OCP\SetupCheck\ISetupCheck;
Expand All @@ -31,32 +31,29 @@ public function getName(): string {
}

public function run(): SetupResult {
$allowedAdminRanges = $this->config->getSystemValue(RemoteIpAddress::SETTING_NAME, false);
if ($allowedAdminRanges === false) {
$allowedAdminRanges = $this->config->getSystemValue(RemoteAddress::SETTING_NAME, false);
if (
$allowedAdminRanges === false
|| (is_array($allowedAdminRanges) && empty($allowedAdminRanges))
) {
return SetupResult::success($this->l10n->t('Admin IP filtering isn’t applied.'));
}

if (!is_array($allowedAdminRanges)) {
return SetupResult::error(
$this->l10n->t(
'Configuration key "%1$s" expects an array (%2$s found). Admin IP range validation will not be applied.',
[RemoteIpAddress::SETTING_NAME, gettype($allowedAdminRanges)],
[RemoteAddress::SETTING_NAME, gettype($allowedAdminRanges)],
)
);
}

$invalidRanges = array_reduce($allowedAdminRanges, static function (array $carry, mixed $range) {
if (!is_string($range) || Factory::parseRangeString($range) === null) {
$carry[] = $range;
}

return $carry;
}, []);
if (count($invalidRanges) > 0) {
$invalidRanges = array_filter($allowedAdminRanges, static fn (mixed $range) => !is_string($range) || !Range::isValid($range));
if (!empty($invalidRanges)) {
return SetupResult::warning(
$this->l10n->t(
'Configuration key "%1$s" contains invalid IP range(s): "%2$s"',
[RemoteIpAddress::SETTING_NAME, implode('", "', $invalidRanges)],
[RemoteAddress::SETTING_NAME, implode('", "', $invalidRanges)],
),
);
}
Expand Down
7 changes: 6 additions & 1 deletion lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,9 @@
'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\\Ip\\IAddress' => $baseDir . '/lib/public/Security/Ip/IAddress.php',
'OCP\\Security\\Ip\\IRange' => $baseDir . '/lib/public/Security/Ip/IRange.php',
'OCP\\Security\\Ip\\IRemoteAddress' => $baseDir . '/lib/public/Security/Ip/IRemoteAddress.php',
'OCP\\Security\\RateLimiting\\ILimiter' => $baseDir . '/lib/public/Security/RateLimiting/ILimiter.php',
'OCP\\Security\\RateLimiting\\IRateLimitExceededException' => $baseDir . '/lib/public/Security/RateLimiting/IRateLimitExceededException.php',
'OCP\\Security\\VerificationToken\\IVerificationToken' => $baseDir . '/lib/public/Security/VerificationToken/IVerificationToken.php',
Expand Down Expand Up @@ -1808,14 +1811,16 @@
'OC\\Security\\IdentityProof\\Key' => $baseDir . '/lib/private/Security/IdentityProof/Key.php',
'OC\\Security\\IdentityProof\\Manager' => $baseDir . '/lib/private/Security/IdentityProof/Manager.php',
'OC\\Security\\IdentityProof\\Signer' => $baseDir . '/lib/private/Security/IdentityProof/Signer.php',
'OC\\Security\\Ip\\Address' => $baseDir . '/lib/private/Security/Ip/Address.php',
'OC\\Security\\Ip\\Range' => $baseDir . '/lib/private/Security/Ip/Range.php',
'OC\\Security\\Ip\\RemoteAddress' => $baseDir . '/lib/private/Security/Ip/RemoteAddress.php',
'OC\\Security\\Normalizer\\IpAddress' => $baseDir . '/lib/private/Security/Normalizer/IpAddress.php',
'OC\\Security\\RateLimiting\\Backend\\DatabaseBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php',
'OC\\Security\\RateLimiting\\Backend\\IBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/IBackend.php',
'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\\RemoteIpAddress' => $baseDir . '/lib/private/Security/RemoteIpAddress.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',
Expand Down
7 changes: 6 additions & 1 deletion lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'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\\Ip\\IAddress' => __DIR__ . '/../../..' . '/lib/public/Security/Ip/IAddress.php',
'OCP\\Security\\Ip\\IRange' => __DIR__ . '/../../..' . '/lib/public/Security/Ip/IRange.php',
'OCP\\Security\\Ip\\IRemoteAddress' => __DIR__ . '/../../..' . '/lib/public/Security/Ip/IRemoteAddress.php',
'OCP\\Security\\RateLimiting\\ILimiter' => __DIR__ . '/../../..' . '/lib/public/Security/RateLimiting/ILimiter.php',
'OCP\\Security\\RateLimiting\\IRateLimitExceededException' => __DIR__ . '/../../..' . '/lib/public/Security/RateLimiting/IRateLimitExceededException.php',
'OCP\\Security\\VerificationToken\\IVerificationToken' => __DIR__ . '/../../..' . '/lib/public/Security/VerificationToken/IVerificationToken.php',
Expand Down Expand Up @@ -1841,14 +1844,16 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Security\\IdentityProof\\Key' => __DIR__ . '/../../..' . '/lib/private/Security/IdentityProof/Key.php',
'OC\\Security\\IdentityProof\\Manager' => __DIR__ . '/../../..' . '/lib/private/Security/IdentityProof/Manager.php',
'OC\\Security\\IdentityProof\\Signer' => __DIR__ . '/../../..' . '/lib/private/Security/IdentityProof/Signer.php',
'OC\\Security\\Ip\\Address' => __DIR__ . '/../../..' . '/lib/private/Security/Ip/Address.php',
'OC\\Security\\Ip\\Range' => __DIR__ . '/../../..' . '/lib/private/Security/Ip/Range.php',
'OC\\Security\\Ip\\RemoteAddress' => __DIR__ . '/../../..' . '/lib/private/Security/Ip/RemoteAddress.php',
'OC\\Security\\Normalizer\\IpAddress' => __DIR__ . '/../../..' . '/lib/private/Security/Normalizer/IpAddress.php',
'OC\\Security\\RateLimiting\\Backend\\DatabaseBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php',
'OC\\Security\\RateLimiting\\Backend\\IBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/IBackend.php',
'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\\RemoteIpAddress' => __DIR__ . '/../../..' . '/lib/private/Security/RemoteIpAddress.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',
Expand Down
4 changes: 2 additions & 2 deletions lib/private/AppFramework/DependencyInjection/DIContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
use OC\Core\Middleware\TwoFactorMiddleware;
use OC\Diagnostics\EventLogger;
use OC\Log\PsrLoggerAdapter;
use OC\Security\RemoteIpAddress;
use OC\ServerContainer;
use OC\Settings\AuthorizedGroupMapper;
use OCA\WorkflowEngine\Manager;
Expand All @@ -47,6 +46,7 @@
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\Ip\IRemoteAddress;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;

Expand Down Expand Up @@ -234,7 +234,7 @@ public function __construct(string $appName, array $urlParams = [], ?ServerConta
$server->getL10N('lib'),
$c->get(AuthorizedGroupMapper::class),
$server->get(IUserSession::class),
$c->get(RemoteIpAddress::class),
$c->get(IRemoteAddress::class),
);
$dispatcher->registerMiddleware($securityMiddleware);
$dispatcher->registerMiddleware(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
use OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException;
use OC\AppFramework\Utility\ControllerMethodReflector;
use OC\Security\RemoteIpAddress;
use OC\Settings\AuthorizedGroupMapper;
use OC\User\Session;
use OCP\App\AppPathNotFoundException;
Expand All @@ -42,6 +41,7 @@
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\Security\Ip\IRemoteAddress;
use OCP\Util;
use Psr\Log\LoggerInterface;
use ReflectionMethod;
Expand All @@ -67,7 +67,7 @@ public function __construct(
private IL10N $l10n,
private AuthorizedGroupMapper $groupAuthorizationMapper,
private IUserSession $userSession,
private RemoteIpAddress $remoteIpAddress,
private IRemoteAddress $remoteAddress,
) {
}

Expand Down Expand Up @@ -134,7 +134,7 @@ public function beforeController($controller, $methodName) {
if (!$authorized) {
throw new NotAdminException($this->l10n->t('Logged in account must be an admin, a sub admin or gotten special right to access this setting'));
}
if (!$this->remoteIpAddress->allowsAdminActions()) {
if (!$this->remoteAddress->allowsAdminActions()) {
throw new AdminIpNotAllowedException($this->l10n->t('Your current IP address doesn’t allow you to perform admin actions'));
}
}
Expand All @@ -151,12 +151,12 @@ public function beforeController($controller, $methodName) {
throw new NotAdminException($this->l10n->t('Logged in account must be an admin'));
}
if ($this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
&& !$this->remoteIpAddress->allowsAdminActions()) {
&& !$this->remoteAddress->allowsAdminActions()) {
throw new AdminIpNotAllowedException($this->l10n->t('Your current IP address doesn’t allow you to perform admin actions'));
}
if (!$this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
&& !$this->hasAnnotationOrAttribute($reflectionMethod, 'NoAdminRequired', NoAdminRequired::class)
&& !$this->remoteIpAddress->allowsAdminActions()) {
&& !$this->remoteAddress->allowsAdminActions()) {
throw new AdminIpNotAllowedException($this->l10n->t('Your current IP address doesn’t allow you to perform admin actions'));
}

Expand Down
6 changes: 3 additions & 3 deletions lib/private/Group/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
namespace OC\Group;

use OC\Hooks\PublicEmitter;
use OC\Security\RemoteIpAddress;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Group\Backend\IBatchMethodsBackend;
use OCP\Group\Backend\ICreateNamedGroupBackend;
Expand All @@ -20,6 +19,7 @@
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\Security\Ip\IRemoteAddress;
use Psr\Log\LoggerInterface;
use function is_string;

Expand Down Expand Up @@ -60,7 +60,7 @@ public function __construct(
private IEventDispatcher $dispatcher,
private LoggerInterface $logger,
ICacheFactory $cacheFactory,
private RemoteIpAddress $remoteIpAddress,
private IRemoteAddress $remoteAddress,
) {
$this->displayNameCache = new DisplayNameCache($cacheFactory, $this);

Expand Down Expand Up @@ -321,7 +321,7 @@ public function getUserIdGroups(string $uid): array {
* @return bool if admin
*/
public function isAdmin($userId) {
if (!$this->remoteIpAddress->allowsAdminActions()) {
if (!$this->remoteAddress->allowsAdminActions()) {
return false;
}

Expand Down
48 changes: 48 additions & 0 deletions lib/private/Security/Ip/Address.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OC\Security\Ip;

use InvalidArgumentException;
use IPLib\Address\AddressInterface;
use IPLib\Factory;
use OCP\Security\Ip\IAddress;
use OCP\Security\Ip\IRange;

/**
* @since 30.0.0
*/
class Address implements IAddress {
private readonly AddressInterface $ip;

public function __construct(string $ip) {
$ip = Factory::parseAddressString($ip);
if ($ip === null) {
throw new InvalidArgumentException('Given IP address can’t be parsed');
}
$this->ip = $ip;
}

public static function isValid(string $ip): bool {
return Factory::parseAddressString($ip) !== null;
}

public function matches(IRange... $ranges): bool {
foreach($ranges as $range) {
if ($range->contains($this)) {
return true;
}
}

return false;
}

public function __toString(): string {
return $this->ip->toString();
}
}
39 changes: 39 additions & 0 deletions lib/private/Security/Ip/Range.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OC\Security\Ip;

use InvalidArgumentException;
use IPLib\Factory;
use IPLib\Range\RangeInterface;
use OCP\Security\Ip\IAddress;
use OCP\Security\Ip\IRange;

class Range implements IRange {
private readonly RangeInterface $range;

public function __construct(string $range) {
$range = Factory::parseRangeString($range);
if ($range === null) {
throw new InvalidArgumentException('Given range can’t be parsed');
}
$this->range = $range;
}

public static function isValid(string $range): bool {
return Factory::parseRangeString($range) !== null;
}

public function contains(IAddress $address): bool {
return $this->range->contains(Factory::parseAddressString((string) $address));
}

public function __toString(): string {
return $this->range->toString();
}
}
71 changes: 71 additions & 0 deletions lib/private/Security/Ip/RemoteAddress.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OC\Security\Ip;

use OCP\IConfig;
use OCP\IRequest;
use OCP\Security\Ip\IAddress;
use OCP\Security\Ip\IRange;
use OCP\Security\Ip\IRemoteAddress;

class RemoteAddress implements IRemoteAddress, IAddress {
public const SETTING_NAME = 'allowed_admin_ranges';

private readonly ?IAddress $ip;

public function __construct(
private IConfig $config,
IRequest $request,
) {
$remoteAddress = $request->getRemoteAddress();
$this->ip = $remoteAddress === ''
? null
: new Address($remoteAddress);
}

public static function isValid(string $ip): bool {
return Address::isValid($ip);
}

public function matches(IRange... $ranges): bool {
return $this->ip === null
? true
: $this->ip->matches(... $ranges);
}

public function allowsAdminActions(): bool {
if ($this->ip === null) {
return true;
}

$allowedAdminRanges = $this->config->getSystemValue(self::SETTING_NAME, false);

// Don't apply restrictions on empty or invalid configuration
if (
$allowedAdminRanges === false
|| !is_array($allowedAdminRanges)
|| empty($allowedAdminRanges)
) {
return true;
}

foreach ($allowedAdminRanges as $allowedAdminRange) {
if ((new Range($allowedAdminRange))->contains($this->ip)) {
return true;
}
}

return false;
}

public function __toString(): string {
return (string) $this->ip;
}
}
Loading

0 comments on commit 17a6845

Please sign in to comment.