Skip to content

Commit

Permalink
Merge pull request #28819 from nextcloud/backport/28728/stable19
Browse files Browse the repository at this point in the history
[stable19] Add database ratelimiting backend
  • Loading branch information
LukasReschke authored Sep 13, 2021
2 parents 358ac8f + f53ae86 commit fd71f49
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 70 deletions.
43 changes: 43 additions & 0 deletions core/Migrations/Version23000Date20210906132259.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace OC\Core\Migrations;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

class Version23000Date20210906132259 extends SimpleMigrationStep {
private const TABLE_NAME = 'ratelimit_entries';

/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();

$hasTable = $schema->hasTable(self::TABLE_NAME);

if (!$hasTable) {
$table = $schema->createTable(self::TABLE_NAME);
$table->addColumn('hash', 'string', [
'notnull' => true,
'length' => 128,
]);
$table->addColumn('delete_after', 'datetime', [
'notnull' => true,
]);
$table->addIndex(['hash'], 'ratelimit_hash');
$table->addIndex(['delete_after'], 'ratelimit_delete_after');
return $schema;
}

return null;
}
}
4 changes: 3 additions & 1 deletion lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,7 @@
'OC\\Core\\Migrations\\Version18000Date20191014105105' => $baseDir . '/core/Migrations/Version18000Date20191014105105.php',
'OC\\Core\\Migrations\\Version18000Date20191204114856' => $baseDir . '/core/Migrations/Version18000Date20191204114856.php',
'OC\\Core\\Migrations\\Version19000Date20200211083441' => $baseDir . '/core/Migrations/Version19000Date20200211083441.php',
'OC\\Core\\Migrations\\Version23000Date20210906132259' => $baseDir . '/core/Migrations/Version23000Date20210906132259.php',
'OC\\Core\\Notification\\RemoveLinkSharesNotifier' => $baseDir . '/core/Notification/RemoveLinkSharesNotifier.php',
'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php',
'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php',
Expand Down Expand Up @@ -1229,8 +1230,9 @@
'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',
Expand Down
4 changes: 3 additions & 1 deletion lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Core\\Migrations\\Version18000Date20191014105105' => __DIR__ . '/../../..' . '/core/Migrations/Version18000Date20191014105105.php',
'OC\\Core\\Migrations\\Version18000Date20191204114856' => __DIR__ . '/../../..' . '/core/Migrations/Version18000Date20191204114856.php',
'OC\\Core\\Migrations\\Version19000Date20200211083441' => __DIR__ . '/../../..' . '/core/Migrations/Version19000Date20200211083441.php',
'OC\\Core\\Migrations\\Version23000Date20210906132259' => __DIR__ . '/../../..' . '/core/Migrations/Version23000Date20210906132259.php',
'OC\\Core\\Notification\\RemoveLinkSharesNotifier' => __DIR__ . '/../../..' . '/core/Notification/RemoveLinkSharesNotifier.php',
'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php',
'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php',
Expand Down Expand Up @@ -1258,8 +1259,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',
Expand Down
122 changes: 122 additions & 0 deletions lib/private/Security/RateLimiting/Backend/DatabaseBackend.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?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
* @return int
*/
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))
)
->execute();

$qb = $this->dbConnection->getQueryBuilder();
$qb->select($qb->func()->count('*', 'attempts'))
->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->execute();
$row = $cursor->fetch();
$cursor->closeCursor();

return (int)$row['attempts'];
}

/**
* {@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),
])
->execute();
}
}
6 changes: 2 additions & 4 deletions lib/private/Security/RateLimiting/Backend/IBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,14 @@
*/
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@
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 */
Expand Down Expand Up @@ -87,16 +87,14 @@ private function getExistingAttempts(string $identifier): array {
* {@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++;
}
}
Expand All @@ -114,16 +112,16 @@ public function registerAttempt(string $methodIdentifier,
$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));
}
}
12 changes: 3 additions & 9 deletions lib/private/Security/RateLimiting/Limiter.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,17 @@
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;
}

/**
Expand All @@ -60,12 +54,12 @@ private function register(string $methodIdentifier,
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);
}

/**
Expand Down
19 changes: 15 additions & 4 deletions lib/private/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -756,10 +756,21 @@ public function __construct($webRoot, \OC\Config $config) {
$this->registerDeprecatedAlias('Search', ISearch::class);

$this->registerService(\OC\Security\RateLimiting\Backend\IBackend::class, function ($c) {
return new \OC\Security\RateLimiting\Backend\MemoryCache(
$this->getMemCacheFactory(),
new \OC\AppFramework\Utility\TimeFactory()
);
$cacheFactory = $this->query(ICacheFactory::class);

if ($cacheFactory->isAvailable()) {
$backend = new \OC\Security\RateLimiting\Backend\MemoryCacheBackend(
$this->query(ICacheFactory::class),
new \OC\AppFramework\Utility\TimeFactory()
);
} else {
$backend = new \OC\Security\RateLimiting\Backend\DatabaseBackend(
$this->query(IDBConnection::class),
new \OC\AppFramework\Utility\TimeFactory()
);
}

return $backend;
});

$this->registerService(\OCP\Security\ISecureRandom::class, function ($c) {
Expand Down
Loading

0 comments on commit fd71f49

Please sign in to comment.