diff options
Diffstat (limited to 'lib/private/Security/Bruteforce')
4 files changed, 40 insertions, 91 deletions
diff --git a/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php b/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php index 0e272d94d0d..33c2a3aae62 100644 --- a/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php +++ b/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php @@ -45,7 +45,7 @@ class DatabaseBackend implements IBackend { $row = $result->fetch(); $result->closeCursor(); - return (int) $row['attempts']; + return (int)$row['attempts']; } /** diff --git a/lib/private/Security/Bruteforce/Backend/MemoryCacheBackend.php b/lib/private/Security/Bruteforce/Backend/MemoryCacheBackend.php index 32571c72fae..9a0723db47e 100644 --- a/lib/private/Security/Bruteforce/Backend/MemoryCacheBackend.php +++ b/lib/private/Security/Bruteforce/Backend/MemoryCacheBackend.php @@ -19,7 +19,7 @@ class MemoryCacheBackend implements IBackend { ICacheFactory $cacheFactory, private ITimeFactory $timeFactory, ) { - $this->cache = $cacheFactory->createDistributed(__CLASS__); + $this->cache = $cacheFactory->createDistributed(self::class); } private function hash( @@ -137,7 +137,7 @@ class MemoryCacheBackend implements IBackend { $existingAttempts = array_values($existingAttempts); // Store the new attempt - $existingAttempts[] = $timestamp . '#' . $this->hash($action) . '#' . $this->hash($metadata); + $existingAttempts[] = $timestamp . '#' . $this->hash($action) . '#' . $this->hash($metadata); $this->cache->set($identifier, json_encode($existingAttempts), 12 * 3600); } diff --git a/lib/private/Security/Bruteforce/CleanupJob.php b/lib/private/Security/Bruteforce/CleanupJob.php index e7d981caa08..f07e4dbacbd 100644 --- a/lib/private/Security/Bruteforce/CleanupJob.php +++ b/lib/private/Security/Bruteforce/CleanupJob.php @@ -9,7 +9,6 @@ declare(strict_types=1); namespace OC\Security\Bruteforce; use OCP\AppFramework\Utility\ITimeFactory; -use OCP\BackgroundJob\IJob; use OCP\BackgroundJob\TimedJob; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; @@ -22,8 +21,8 @@ class CleanupJob extends TimedJob { parent::__construct($time); // Run once a day - $this->setInterval(3600 * 24); - $this->setTimeSensitivity(IJob::TIME_INSENSITIVE); + $this->setInterval(60 * 60 * 24); + $this->setTimeSensitivity(self::TIME_INSENSITIVE); } protected function run($argument): void { @@ -33,6 +32,6 @@ class CleanupJob extends TimedJob { $qb = $this->connection->getQueryBuilder(); $qb->delete('bruteforce_attempts') ->where($qb->expr()->lt('occurred', $qb->createNamedParameter($time), IQueryBuilder::PARAM_INT)); - $qb->execute(); + $qb->executeStatement(); } } diff --git a/lib/private/Security/Bruteforce/Throttler.php b/lib/private/Security/Bruteforce/Throttler.php index 7e310035ce4..574f6c80c3f 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); } /** @@ -176,7 +113,7 @@ class Throttler implements IThrottler { return 0; } - $maxAgeTimestamp = (int) ($this->timeFactory->getTime() - 3600 * $maxAgeHours); + $maxAgeTimestamp = (int)($this->timeFactory->getTime() - 3600 * $maxAgeHours); return $this->backend->getAttempts( $ipAddress->getSubnet(), @@ -190,12 +127,19 @@ class Throttler implements IThrottler { */ public function getDelay(string $ip, string $action = ''): int { $attempts = $this->getAttempts($ip, $action); + return $this->calculateDelay($attempts); + } + + /** + * {@inheritDoc} + */ + public function calculateDelay(int $attempts): int { if ($attempts === 0) { return 0; } $firstDelay = 0.1; - if ($attempts > self::MAX_ATTEMPTS) { + if ($attempts > $this->config->getSystemValueInt('auth.bruteforce.max-attempts', self::MAX_ATTEMPTS)) { // Don't ever overflow. Just assume the maxDelay time:s return self::MAX_DELAY_MS; } @@ -204,7 +148,7 @@ class Throttler implements IThrottler { if ($delay > self::MAX_DELAY) { return self::MAX_DELAY_MS; } - return (int) \ceil($delay * 1000); + return (int)\ceil($delay * 1000); } /** @@ -262,25 +206,31 @@ class Throttler implements IThrottler { * {@inheritDoc} */ public function sleepDelayOrThrowOnMax(string $ip, string $action = ''): int { - $delay = $this->getDelay($ip, $action); - if (($delay === self::MAX_DELAY_MS) && $this->getAttempts($ip, $action, 0.5) > self::MAX_ATTEMPTS) { - $this->logger->info('IP address blocked because it reached the maximum failed attempts in the last 30 minutes [action: {action}, ip: {ip}]', [ - 'action' => $action, - 'ip' => $ip, - ]); - // If the ip made too many attempts within the last 30 mins we don't execute anymore - throw new MaxDelayReached('Reached maximum delay'); - } - if ($delay > 100) { - $this->logger->info('IP address throttled because it reached the attempts limit in the last 30 minutes [action: {action}, delay: {delay}, ip: {ip}]', [ + $maxAttempts = $this->config->getSystemValueInt('auth.bruteforce.max-attempts', self::MAX_ATTEMPTS); + $attempts = $this->getAttempts($ip, $action); + if ($attempts > $maxAttempts) { + $attempts30mins = $this->getAttempts($ip, $action, 0.5); + if ($attempts30mins > $maxAttempts) { + $this->logger->info('IP address blocked because it reached the maximum failed attempts in the last 30 minutes [action: {action}, attempts: {attempts}, ip: {ip}]', [ + 'action' => $action, + 'ip' => $ip, + 'attempts' => $attempts30mins, + ]); + // If the ip made too many attempts within the last 30 mins we don't execute anymore + throw new MaxDelayReached('Reached maximum delay'); + } + + $this->logger->info('IP address throttled because it reached the attempts limit in the last 12 hours [action: {action}, attempts: {attempts}, ip: {ip}]', [ 'action' => $action, 'ip' => $ip, - 'delay' => $delay, + 'attempts' => $attempts, ]); } - if (!$this->config->getSystemValueBool('auth.bruteforce.protection.testing')) { - usleep($delay * 1000); + + if ($attempts > 0) { + return $this->calculateDelay($attempts); } - return $delay; + + return 0; } } |