diff options
Diffstat (limited to 'lib/private')
-rw-r--r-- | lib/private/AppFramework/Middleware/Security/BruteForceMiddleware.php | 29 | ||||
-rw-r--r-- | lib/private/Security/Bruteforce/Throttler.php | 89 | ||||
-rw-r--r-- | lib/private/legacy/OC_Template.php | 2 |
3 files changed, 93 insertions, 27 deletions
diff --git a/lib/private/AppFramework/Middleware/Security/BruteForceMiddleware.php b/lib/private/AppFramework/Middleware/Security/BruteForceMiddleware.php index 398c2f3f3a4..e6c511537a0 100644 --- a/lib/private/AppFramework/Middleware/Security/BruteForceMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/BruteForceMiddleware.php @@ -1,4 +1,6 @@ <?php + +declare(strict_types=1); /** * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> * @@ -26,9 +28,15 @@ namespace OC\AppFramework\Middleware\Security; use OC\AppFramework\Utility\ControllerMethodReflector; use OC\Security\Bruteforce\Throttler; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Http\TooManyRequestsResponse; use OCP\AppFramework\Middleware; +use OCP\AppFramework\OCS\OCSException; +use OCP\AppFramework\OCSController; use OCP\IRequest; +use OCP\Security\Bruteforce\MaxDelayReached; /** * Class BruteForceMiddleware performs the bruteforce protection for controllers @@ -66,7 +74,7 @@ class BruteForceMiddleware extends Middleware { if ($this->reflector->hasAnnotation('BruteForceProtection')) { $action = $this->reflector->getAnnotationParameter('BruteForceProtection', 'action'); - $this->throttler->sleepDelay($this->request->getRemoteAddress(), $action); + $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), $action); } } @@ -83,4 +91,23 @@ class BruteForceMiddleware extends Middleware { return parent::afterController($controller, $methodName, $response); } + + /** + * @param Controller $controller + * @param string $methodName + * @param \Exception $exception + * @throws \Exception + * @return Response + */ + public function afterException($controller, $methodName, \Exception $exception): Response { + if ($exception instanceof MaxDelayReached) { + if ($controller instanceof OCSController) { + throw new OCSException($exception->getMessage(), Http::STATUS_TOO_MANY_REQUESTS); + } + + return new TooManyRequestsResponse(); + } + + throw $exception; + } } diff --git a/lib/private/Security/Bruteforce/Throttler.php b/lib/private/Security/Bruteforce/Throttler.php index 63c6361b9ce..169ad0c0623 100644 --- a/lib/private/Security/Bruteforce/Throttler.php +++ b/lib/private/Security/Bruteforce/Throttler.php @@ -1,4 +1,6 @@ <?php + +declare(strict_types=1); /** * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> * @@ -34,6 +36,7 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\IConfig; use OCP\IDBConnection; use OCP\ILogger; +use OCP\Security\Bruteforce\MaxDelayReached; /** * Class Throttler implements the bruteforce protection for security actions in @@ -50,6 +53,9 @@ use OCP\ILogger; */ class Throttler { public const LOGIN_ACTION = 'login'; + public const MAX_DELAY = 25; + public const MAX_DELAY_MS = 25000; // in milliseconds + public const MAX_ATTEMPTS = 10; /** @var IDBConnection */ private $db; @@ -82,7 +88,7 @@ class Throttler { * @param int $expire * @return \DateInterval */ - private function getCutoff($expire) { + private function getCutoff(int $expire): \DateInterval { $d1 = new \DateTime(); $d2 = clone $d1; $d2->sub(new \DateInterval('PT' . $expire . 'S')); @@ -92,11 +98,12 @@ class Throttler { /** * Calculate the cut off timestamp * + * @param float $maxAgeHours * @return int */ - private function getCutoffTimestamp(): int { + private function getCutoffTimestamp(float $maxAgeHours = 12.0): int { return (new \DateTime()) - ->sub($this->getCutoff(43200)) + ->sub($this->getCutoff((int) ($maxAgeHours * 3600))) ->getTimestamp(); } @@ -108,9 +115,9 @@ class Throttler { * @param array $metadata Optional metadata logged to the database * @suppress SqlInjectionChecker */ - public function registerAttempt($action, - $ip, - array $metadata = []) { + public function registerAttempt(string $action, + string $ip, + array $metadata = []): void { // No need to log if the bruteforce protection is disabled if ($this->config->getSystemValue('auth.bruteforce.protection.enabled', true) === false) { return; @@ -150,15 +157,14 @@ class Throttler { * @param string $ip * @return bool */ - private function isIPWhitelisted($ip) { + private function isIPWhitelisted(string $ip): bool { if ($this->config->getSystemValue('auth.bruteforce.protection.enabled', true) === false) { return true; } $keys = $this->config->getAppKeys('bruteForce'); $keys = array_filter($keys, function ($key) { - $regex = '/^whitelist_/S'; - return preg_match($regex, $key) === 1; + return 0 === strpos($key, 'whitelist_'); }); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { @@ -215,18 +221,19 @@ class Throttler { * * @param string $ip * @param string $action optionally filter by action + * @param float $maxAgeHours * @return int */ - public function getDelay($ip, $action = '') { + public function getAttempts(string $ip, string $action = '', float $maxAgeHours = 12): int { $ipAddress = new IpAddress($ip); if ($this->isIPWhitelisted((string)$ipAddress)) { return 0; } - $cutoffTime = $this->getCutoffTimestamp(); + $cutoffTime = $this->getCutoffTimestamp($maxAgeHours); $qb = $this->db->getQueryBuilder(); - $qb->select('*') + $qb->select($qb->func()->count('*', 'attempts')) ->from('bruteforce_attempts') ->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime))) ->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($ipAddress->getSubnet()))); @@ -235,24 +242,37 @@ class Throttler { $qb->andWhere($qb->expr()->eq('action', $qb->createNamedParameter($action))); } - $attempts = count($qb->execute()->fetchAll()); + $result = $qb->execute(); + $row = $result->fetch(); + $result->closeCursor(); + + return (int) $row['attempts']; + } + /** + * Get the throttling delay (in milliseconds) + * + * @param string $ip + * @param string $action optionally filter by action + * @return int + */ + public function getDelay(string $ip, string $action = ''): int { + $attempts = $this->getAttempts($ip, $action); if ($attempts === 0) { return 0; } - $maxDelay = 25; $firstDelay = 0.1; - if ($attempts > (8 * PHP_INT_SIZE - 1)) { + if ($attempts > self::MAX_ATTEMPTS) { // Don't ever overflow. Just assume the maxDelay time:s - $firstDelay = $maxDelay; - } else { - $firstDelay *= pow(2, $attempts); - if ($firstDelay > $maxDelay) { - $firstDelay = $maxDelay; - } + return self::MAX_DELAY_MS; } - return (int) \ceil($firstDelay * 1000); + + $delay = $firstDelay * 2**$attempts; + if ($delay > self::MAX_DELAY) { + return self::MAX_DELAY_MS; + } + return (int) \ceil($delay * 1000); } /** @@ -260,9 +280,9 @@ class Throttler { * * @param string $ip * @param string $action - * @param string $metadata + * @param array $metadata */ - public function resetDelay($ip, $action, $metadata) { + public function resetDelay(string $ip, string $action, array $metadata): void { $ipAddress = new IpAddress($ip); if ($this->isIPWhitelisted((string)$ipAddress)) { return; @@ -303,9 +323,28 @@ class Throttler { * @param string $action optionally filter by action * @return int the time spent sleeping */ - public function sleepDelay($ip, $action = '') { + public function sleepDelay(string $ip, string $action = ''): int { $delay = $this->getDelay($ip, $action); usleep($delay * 1000); return $delay; } + + /** + * Will sleep for the defined amount of time unless maximum was reached in the last 30 minutes + * In this case a "429 Too Many Request" exception is thrown + * + * @param string $ip + * @param string $action optionally filter by action + * @return int the time spent sleeping + * @throws MaxDelayReached when reached the maximum + */ + 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) { + // If the ip made too many attempts within the last 30 mins we don't execute anymore + throw new MaxDelayReached('Reached maximum delay'); + } + usleep($delay * 1000); + return $delay; + } } diff --git a/lib/private/legacy/OC_Template.php b/lib/private/legacy/OC_Template.php index 18a15ad1d43..54c203a3ab6 100644 --- a/lib/private/legacy/OC_Template.php +++ b/lib/private/legacy/OC_Template.php @@ -171,7 +171,7 @@ class OC_Template extends \OC\Template\Base { /** * Process the template - * @return boolean|string + * @return string * * This function process the template. If $this->renderAs is set, it * will produce a full page. |