summaryrefslogtreecommitdiffstats
path: root/lib/private
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private')
-rw-r--r--lib/private/AppFramework/Middleware/Security/BruteForceMiddleware.php29
-rw-r--r--lib/private/Security/Bruteforce/Throttler.php89
-rw-r--r--lib/private/legacy/OC_Template.php2
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.