aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorLukas Reschke <lukas@statuscode.ch>2021-09-13 13:07:37 +0200
committerGitHub <noreply@github.com>2021-09-13 13:07:37 +0200
commit0dcc5c0e9f346912ed8c263950ad91f35a41bff8 (patch)
treec360c1c57b53a007ffec586428901ad1669b2dad /lib
parentd31456093d3bb2ff55fe0fde07050f07301459d6 (diff)
parent474a5b55d39e621c12947b50ea6c8febfceed7d7 (diff)
downloadnextcloud-server-0dcc5c0e9f346912ed8c263950ad91f35a41bff8.tar.gz
nextcloud-server-0dcc5c0e9f346912ed8c263950ad91f35a41bff8.zip
Merge pull request #28728 from nextcloud/add-database-backend-limiter
Add database ratelimiting backend
Diffstat (limited to 'lib')
-rw-r--r--lib/composer/composer/autoload_classmap.php4
-rw-r--r--lib/composer/composer/autoload_static.php4
-rw-r--r--lib/private/Security/RateLimiting/Backend/DatabaseBackend.php124
-rw-r--r--lib/private/Security/RateLimiting/Backend/IBackend.php6
-rw-r--r--lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php (renamed from lib/private/Security/RateLimiting/Backend/MemoryCache.php)20
-rw-r--r--lib/private/Security/RateLimiting/Limiter.php12
-rw-r--r--lib/private/Server.php18
7 files changed, 158 insertions, 30 deletions
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 3f1f0d3b336..caddb528d5a 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -971,6 +971,7 @@ return array(
'OC\\Core\\Migrations\\Version21000Date20210309185126' => $baseDir . '/core/Migrations/Version21000Date20210309185126.php',
'OC\\Core\\Migrations\\Version21000Date20210309185127' => $baseDir . '/core/Migrations/Version21000Date20210309185127.php',
'OC\\Core\\Migrations\\Version22000Date20210216080825' => $baseDir . '/core/Migrations/Version22000Date20210216080825.php',
+ 'OC\\Core\\Migrations\\Version23000Date20210906132259' => $baseDir . '/core/Migrations/Version23000Date20210906132259.php',
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php',
'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php',
@@ -1367,8 +1368,9 @@ return array(
'OC\\Security\\IdentityProof\\Manager' => $baseDir . '/lib/private/Security/IdentityProof/Manager.php',
'OC\\Security\\IdentityProof\\Signer' => $baseDir . '/lib/private/Security/IdentityProof/Signer.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\\MemoryCache' => $baseDir . '/lib/private/Security/RateLimiting/Backend/MemoryCache.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\\SecureRandom' => $baseDir . '/lib/private/Security/SecureRandom.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index a5b624e6e6c..727fe0fc956 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -1000,6 +1000,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Core\\Migrations\\Version21000Date20210309185126' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20210309185126.php',
'OC\\Core\\Migrations\\Version21000Date20210309185127' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20210309185127.php',
'OC\\Core\\Migrations\\Version22000Date20210216080825' => __DIR__ . '/../../..' . '/core/Migrations/Version22000Date20210216080825.php',
+ 'OC\\Core\\Migrations\\Version23000Date20210906132259' => __DIR__ . '/../../..' . '/core/Migrations/Version23000Date20210906132259.php',
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php',
'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php',
@@ -1396,8 +1397,9 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Security\\IdentityProof\\Manager' => __DIR__ . '/../../..' . '/lib/private/Security/IdentityProof/Manager.php',
'OC\\Security\\IdentityProof\\Signer' => __DIR__ . '/../../..' . '/lib/private/Security/IdentityProof/Signer.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\\MemoryCache' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/MemoryCache.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\\SecureRandom' => __DIR__ . '/../../..' . '/lib/private/Security/SecureRandom.php',
diff --git a/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php b/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php
new file mode 100644
index 00000000000..1415b5c4131
--- /dev/null
+++ b/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php
@@ -0,0 +1,124 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2021 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @author Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OC\Security\RateLimiting\Backend;
+
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+
+class DatabaseBackend implements IBackend {
+ private const TABLE_NAME = 'ratelimit_entries';
+
+ /** @var IDBConnection */
+ private $dbConnection;
+ /** @var ITimeFactory */
+ private $timeFactory;
+
+ /**
+ * @param IDBConnection $dbConnection
+ * @param ITimeFactory $timeFactory
+ */
+ public function __construct(
+ IDBConnection $dbConnection,
+ ITimeFactory $timeFactory
+ ) {
+ $this->dbConnection = $dbConnection;
+ $this->timeFactory = $timeFactory;
+ }
+
+ /**
+ * @param string $methodIdentifier
+ * @param string $userIdentifier
+ * @return string
+ */
+ private function hash(string $methodIdentifier,
+ string $userIdentifier): string {
+ return hash('sha512', $methodIdentifier . $userIdentifier);
+ }
+
+ /**
+ * @param string $identifier
+ * @param int $seconds
+ * @return int
+ * @throws \OCP\DB\Exception
+ */
+ private function getExistingAttemptCount(
+ string $identifier
+ ): int {
+ $currentTime = $this->timeFactory->getDateTime();
+
+ $qb = $this->dbConnection->getQueryBuilder();
+ $qb->delete(self::TABLE_NAME)
+ ->where(
+ $qb->expr()->lte('delete_after', $qb->createNamedParameter($currentTime, IQueryBuilder::PARAM_DATE))
+ )
+ ->executeStatement();
+
+ $qb = $this->dbConnection->getQueryBuilder();
+ $qb->select($qb->func()->count())
+ ->from(self::TABLE_NAME)
+ ->where(
+ $qb->expr()->eq('hash', $qb->createNamedParameter($identifier, IQueryBuilder::PARAM_STR))
+ )
+ ->andWhere(
+ $qb->expr()->gte('delete_after', $qb->createNamedParameter($currentTime, IQueryBuilder::PARAM_DATE))
+ );
+
+ $cursor = $qb->executeQuery();
+ $row = $cursor->fetchOne();
+ $cursor->closeCursor();
+
+ return (int)$row;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getAttempts(string $methodIdentifier,
+ string $userIdentifier): int {
+ $identifier = $this->hash($methodIdentifier, $userIdentifier);
+ return $this->getExistingAttemptCount($identifier);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function registerAttempt(string $methodIdentifier,
+ string $userIdentifier,
+ int $period) {
+ $identifier = $this->hash($methodIdentifier, $userIdentifier);
+ $deleteAfter = $this->timeFactory->getDateTime()->add(new \DateInterval("PT{$period}S"));
+
+ $qb = $this->dbConnection->getQueryBuilder();
+
+ $qb->insert(self::TABLE_NAME)
+ ->values([
+ 'hash' => $qb->createNamedParameter($identifier, IQueryBuilder::PARAM_STR),
+ 'delete_after' => $qb->createNamedParameter($deleteAfter, IQueryBuilder::PARAM_DATE),
+ ])
+ ->executeStatement();
+ }
+}
diff --git a/lib/private/Security/RateLimiting/Backend/IBackend.php b/lib/private/Security/RateLimiting/Backend/IBackend.php
index d87f53311b2..960bfd2d159 100644
--- a/lib/private/Security/RateLimiting/Backend/IBackend.php
+++ b/lib/private/Security/RateLimiting/Backend/IBackend.php
@@ -35,16 +35,14 @@ namespace OC\Security\RateLimiting\Backend;
*/
interface IBackend {
/**
- * Gets the amount of attempts within the last specified seconds
+ * Gets the number of attempts for the specified method
*
* @param string $methodIdentifier Identifier for the method
* @param string $userIdentifier Identifier for the user
- * @param int $seconds Seconds to look back at
* @return int
*/
public function getAttempts(string $methodIdentifier,
- string $userIdentifier,
- int $seconds): int;
+ string $userIdentifier): int;
/**
* Registers an attempt
diff --git a/lib/private/Security/RateLimiting/Backend/MemoryCache.php b/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php
index 0dab25e4048..f4880fb239c 100644
--- a/lib/private/Security/RateLimiting/Backend/MemoryCache.php
+++ b/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php
@@ -33,12 +33,12 @@ use OCP\ICache;
use OCP\ICacheFactory;
/**
- * Class MemoryCache uses the configured distributed memory cache for storing
+ * Class MemoryCacheBackend uses the configured distributed memory cache for storing
* rate limiting data.
*
* @package OC\Security\RateLimiting\Backend
*/
-class MemoryCache implements IBackend {
+class MemoryCacheBackend implements IBackend {
/** @var ICache */
private $cache;
/** @var ITimeFactory */
@@ -86,16 +86,14 @@ class MemoryCache implements IBackend {
* {@inheritDoc}
*/
public function getAttempts(string $methodIdentifier,
- string $userIdentifier,
- int $seconds): int {
+ string $userIdentifier): int {
$identifier = $this->hash($methodIdentifier, $userIdentifier);
$existingAttempts = $this->getExistingAttempts($identifier);
$count = 0;
$currentTime = $this->timeFactory->getTime();
- /** @var array $existingAttempts */
- foreach ($existingAttempts as $attempt) {
- if (($attempt + $seconds) > $currentTime) {
+ foreach ($existingAttempts as $expirationTime) {
+ if ($expirationTime > $currentTime) {
$count++;
}
}
@@ -113,16 +111,16 @@ class MemoryCache implements IBackend {
$existingAttempts = $this->getExistingAttempts($identifier);
$currentTime = $this->timeFactory->getTime();
- // Unset all attempts older than $period
- foreach ($existingAttempts as $key => $attempt) {
- if (($attempt + $period) < $currentTime) {
+ // Unset all attempts that are already expired
+ foreach ($existingAttempts as $key => $expirationTime) {
+ if ($expirationTime < $currentTime) {
unset($existingAttempts[$key]);
}
}
$existingAttempts = array_values($existingAttempts);
// Store the new attempt
- $existingAttempts[] = (string)$currentTime;
+ $existingAttempts[] = (string)($currentTime + $period);
$this->cache->set($identifier, json_encode($existingAttempts));
}
}
diff --git a/lib/private/Security/RateLimiting/Limiter.php b/lib/private/Security/RateLimiting/Limiter.php
index ede72e887fc..91657452d99 100644
--- a/lib/private/Security/RateLimiting/Limiter.php
+++ b/lib/private/Security/RateLimiting/Limiter.php
@@ -29,23 +29,17 @@ namespace OC\Security\RateLimiting;
use OC\Security\Normalizer\IpAddress;
use OC\Security\RateLimiting\Backend\IBackend;
use OC\Security\RateLimiting\Exception\RateLimitExceededException;
-use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IUser;
class Limiter {
/** @var IBackend */
private $backend;
- /** @var ITimeFactory */
- private $timeFactory;
/**
- * @param ITimeFactory $timeFactory
* @param IBackend $backend
*/
- public function __construct(ITimeFactory $timeFactory,
- IBackend $backend) {
+ public function __construct(IBackend $backend) {
$this->backend = $backend;
- $this->timeFactory = $timeFactory;
}
/**
@@ -59,12 +53,12 @@ class Limiter {
string $userIdentifier,
int $period,
int $limit): void {
- $existingAttempts = $this->backend->getAttempts($methodIdentifier, $userIdentifier, $period);
+ $existingAttempts = $this->backend->getAttempts($methodIdentifier, $userIdentifier);
if ($existingAttempts >= $limit) {
throw new RateLimitExceededException();
}
- $this->backend->registerAttempt($methodIdentifier, $userIdentifier, $this->timeFactory->getTime());
+ $this->backend->registerAttempt($methodIdentifier, $userIdentifier, $period);
}
/**
diff --git a/lib/private/Server.php b/lib/private/Server.php
index 6b6a1402a04..a9ea07c27d3 100644
--- a/lib/private/Server.php
+++ b/lib/private/Server.php
@@ -787,10 +787,20 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerDeprecatedAlias('Search', ISearch::class);
$this->registerService(\OC\Security\RateLimiting\Backend\IBackend::class, function ($c) {
- return new \OC\Security\RateLimiting\Backend\MemoryCache(
- $this->get(ICacheFactory::class),
- new \OC\AppFramework\Utility\TimeFactory()
- );
+ $cacheFactory = $c->get(ICacheFactory::class);
+ if ($cacheFactory->isAvailable()) {
+ $backend = new \OC\Security\RateLimiting\Backend\MemoryCacheBackend(
+ $this->get(ICacheFactory::class),
+ new \OC\AppFramework\Utility\TimeFactory()
+ );
+ } else {
+ $backend = new \OC\Security\RateLimiting\Backend\DatabaseBackend(
+ $c->get(IDBConnection::class),
+ new \OC\AppFramework\Utility\TimeFactory()
+ );
+ }
+
+ return $backend;
});
$this->registerAlias(\OCP\Security\ISecureRandom::class, SecureRandom::class);