diff options
author | Joas Schilling <coding@schilljs.com> | 2025-01-17 15:01:07 +0100 |
---|---|---|
committer | Joas Schilling <coding@schilljs.com> | 2025-01-27 12:46:15 +0100 |
commit | c1655bcde75a125fde9a92877907a10ed25297a3 (patch) | |
tree | a73a8bc0952a468d98dcadbe7455145caf335f59 /lib/private | |
parent | f5cd0cbd7007e42635914f3acc69d5e85b0b5907 (diff) | |
download | nextcloud-server-c1655bcde75a125fde9a92877907a10ed25297a3.tar.gz nextcloud-server-c1655bcde75a125fde9a92877907a10ed25297a3.zip |
fix(ratelimit): Allow to bypass rate-limit from bruteforce allowlistbugfix/noid/allow-ratelimit-bypass
Signed-off-by: Joas Schilling <coding@schilljs.com>
Diffstat (limited to 'lib/private')
4 files changed, 73 insertions, 75 deletions
diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index 2083a0e7195..be4f6897da8 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -274,15 +274,7 @@ class DIContainer extends SimpleContainer implements IAppContainer { $c->get(LoggerInterface::class) ) ); - $dispatcher->registerMiddleware( - new RateLimitingMiddleware( - $c->get(IRequest::class), - $c->get(IUserSession::class), - $c->get(IControllerMethodReflector::class), - $c->get(OC\Security\RateLimiting\Limiter::class), - $c->get(ISession::class) - ) - ); + $dispatcher->registerMiddleware($c->get(RateLimitingMiddleware::class)); $dispatcher->registerMiddleware( new OC\AppFramework\Middleware\PublicShare\PublicShareMiddleware( $c->get(IRequest::class), diff --git a/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php b/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php index f4d120ebc30..2d19be97993 100644 --- a/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace OC\AppFramework\Middleware\Security; use OC\AppFramework\Utility\ControllerMethodReflector; +use OC\Security\Ip\BruteforceAllowList; use OC\Security\RateLimiting\Exception\RateLimitExceededException; use OC\Security\RateLimiting\Limiter; use OC\User\Session; @@ -20,6 +21,7 @@ use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Middleware; +use OCP\IAppConfig; use OCP\IRequest; use OCP\ISession; use OCP\IUserSession; @@ -53,6 +55,8 @@ class RateLimitingMiddleware extends Middleware { protected ControllerMethodReflector $reflector, protected Limiter $limiter, protected ISession $session, + protected IAppConfig $appConfig, + protected BruteforceAllowList $bruteForceAllowList, ) { } @@ -73,6 +77,11 @@ class RateLimitingMiddleware extends Middleware { $rateLimit = $this->readLimitFromAnnotationOrAttribute($controller, $methodName, 'UserRateThrottle', UserRateLimit::class); if ($rateLimit !== null) { + if ($this->appConfig->getValueBool('bruteforcesettings', 'apply_allowlist_to_ratelimit') + && $this->bruteForceAllowList->isBypassListed($this->request->getRemoteAddress())) { + return; + } + $this->limiter->registerUserRequest( $rateLimitIdentifier, $rateLimit->getLimit(), diff --git a/lib/private/Security/Bruteforce/Throttler.php b/lib/private/Security/Bruteforce/Throttler.php index 924ae3685f3..21d50848641 100644 --- a/lib/private/Security/Bruteforce/Throttler.php +++ b/lib/private/Security/Bruteforce/Throttler.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace OC\Security\Bruteforce; use OC\Security\Bruteforce\Backend\IBackend; +use OC\Security\Ip\BruteforceAllowList; use OC\Security\Normalizer\IpAddress; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IConfig; @@ -32,14 +33,13 @@ use Psr\Log\LoggerInterface; class Throttler implements IThrottler { /** @var bool[] */ private array $hasAttemptsDeleted = []; - /** @var bool[] */ - private array $ipIsWhitelisted = []; public function __construct( private ITimeFactory $timeFactory, private LoggerInterface $logger, private IConfig $config, private IBackend $backend, + private BruteforceAllowList $allowList, ) { } @@ -83,70 +83,7 @@ class Throttler implements IThrottler { * Check if the IP is whitelisted */ public function isBypassListed(string $ip): bool { - if (isset($this->ipIsWhitelisted[$ip])) { - return $this->ipIsWhitelisted[$ip]; - } - - if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) { - $this->ipIsWhitelisted[$ip] = true; - return true; - } - - $keys = $this->config->getAppKeys('bruteForce'); - $keys = array_filter($keys, function ($key) { - return str_starts_with($key, 'whitelist_'); - }); - - if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { - $type = 4; - } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { - $type = 6; - } else { - $this->ipIsWhitelisted[$ip] = false; - return false; - } - - $ip = inet_pton($ip); - - foreach ($keys as $key) { - $cidr = $this->config->getAppValue('bruteForce', $key, null); - - $cx = explode('/', $cidr); - $addr = $cx[0]; - $mask = (int)$cx[1]; - - // Do not compare ipv4 to ipv6 - if (($type === 4 && !filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) || - ($type === 6 && !filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) { - continue; - } - - $addr = inet_pton($addr); - - $valid = true; - for ($i = 0; $i < $mask; $i++) { - $part = ord($addr[(int)($i / 8)]); - $orig = ord($ip[(int)($i / 8)]); - - $bitmask = 1 << (7 - ($i % 8)); - - $part = $part & $bitmask; - $orig = $orig & $bitmask; - - if ($part !== $orig) { - $valid = false; - break; - } - } - - if ($valid === true) { - $this->ipIsWhitelisted[$ip] = true; - return true; - } - } - - $this->ipIsWhitelisted[$ip] = false; - return false; + return $this->allowList->isBypassListed($ip); } /** diff --git a/lib/private/Security/Ip/BruteforceAllowList.php b/lib/private/Security/Ip/BruteforceAllowList.php new file mode 100644 index 00000000000..cc4f0ceebe5 --- /dev/null +++ b/lib/private/Security/Ip/BruteforceAllowList.php @@ -0,0 +1,60 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Security\Ip; + +use OCP\IAppConfig; +use OCP\Security\Ip\IFactory; + +class BruteforceAllowList { + /** @var array<string, bool> */ + protected array $ipIsAllowListed = []; + + public function __construct( + private readonly IAppConfig $appConfig, + private readonly IFactory $factory, + ) { + } + + /** + * Check if the IP is allowed to bypass bruteforce protection + */ + public function isBypassListed(string $ip): bool { + if (isset($this->ipIsAllowListed[$ip])) { + return $this->ipIsAllowListed[$ip]; + } + + try { + $address = $this->factory->addressFromString($ip); + } catch (\InvalidArgumentException) { + $this->ipIsAllowListed[$ip] = false; + return false; + } + + $keys = $this->appConfig->getKeys('bruteForce'); + $keys = array_filter($keys, static fn ($key): bool => str_starts_with($key, 'whitelist_')); + + foreach ($keys as $key) { + $rangeString = $this->appConfig->getValueString('bruteForce', $key); + try { + $range = $this->factory->rangeFromString($rangeString); + } catch (\InvalidArgumentException) { + continue; + } + + $allowed = $range->contains($address); + if ($allowed) { + $this->ipIsAllowListed[$ip] = true; + return true; + } + } + + $this->ipIsAllowListed[$ip] = false; + return false; + } +} |