Skip to content

Commit 7eb702e

Browse files
committed
Add database ratelimiting backend
In case no distributed memory cache is specified this adds a database backend for ratelimit purposes. Signed-off-by: Lukas Reschke <lukas@statuscode.ch>
1 parent f8c6c26 commit 7eb702e

12 files changed

Lines changed: 227 additions & 72 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OC\Core\Migrations;
6+
7+
use Closure;
8+
use OCP\DB\ISchemaWrapper;
9+
use OCP\DB\Types;
10+
use OCP\Migration\IOutput;
11+
use OCP\Migration\SimpleMigrationStep;
12+
13+
class Version23000Date20210906132259 extends SimpleMigrationStep {
14+
private const TABLE_NAME = 'ratelimit_entries';
15+
16+
/**
17+
* @param IOutput $output
18+
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
19+
* @param array $options
20+
* @return null|ISchemaWrapper
21+
*/
22+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
23+
/** @var ISchemaWrapper $schema */
24+
$schema = $schemaClosure();
25+
26+
$hasTable = $schema->hasTable(self::TABLE_NAME);
27+
28+
if (!$hasTable) {
29+
$table = $schema->createTable(self::TABLE_NAME);
30+
$table->addColumn('hash', Types::STRING, [
31+
'notnull' => true,
32+
'length' => 128,
33+
]);
34+
$table->addColumn('delete_after', Types::DATETIME, [
35+
'notnull' => true,
36+
]);
37+
$table->addIndex(['hash'], 'ratelimit_hash');
38+
$table->addIndex(['delete_after'], 'ratelimit_delete_after');
39+
return $schema;
40+
}
41+
42+
return null;
43+
}
44+
}

lib/composer/composer/autoload_classmap.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -970,6 +970,7 @@
970970
'OC\\Core\\Migrations\\Version21000Date20210309185126' => $baseDir . '/core/Migrations/Version21000Date20210309185126.php',
971971
'OC\\Core\\Migrations\\Version21000Date20210309185127' => $baseDir . '/core/Migrations/Version21000Date20210309185127.php',
972972
'OC\\Core\\Migrations\\Version22000Date20210216080825' => $baseDir . '/core/Migrations/Version22000Date20210216080825.php',
973+
'OC\\Core\\Migrations\\Version23000Date20210906132259' => $baseDir . '/core/Migrations/Version23000Date20210906132259.php',
973974
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
974975
'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php',
975976
'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php',
@@ -1367,8 +1368,9 @@
13671368
'OC\\Security\\IdentityProof\\Manager' => $baseDir . '/lib/private/Security/IdentityProof/Manager.php',
13681369
'OC\\Security\\IdentityProof\\Signer' => $baseDir . '/lib/private/Security/IdentityProof/Signer.php',
13691370
'OC\\Security\\Normalizer\\IpAddress' => $baseDir . '/lib/private/Security/Normalizer/IpAddress.php',
1371+
'OC\\Security\\RateLimiting\\Backend\\DatabaseBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php',
13701372
'OC\\Security\\RateLimiting\\Backend\\IBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/IBackend.php',
1371-
'OC\\Security\\RateLimiting\\Backend\\MemoryCache' => $baseDir . '/lib/private/Security/RateLimiting/Backend/MemoryCache.php',
1373+
'OC\\Security\\RateLimiting\\Backend\\MemoryCacheBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php',
13721374
'OC\\Security\\RateLimiting\\Exception\\RateLimitExceededException' => $baseDir . '/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php',
13731375
'OC\\Security\\RateLimiting\\Limiter' => $baseDir . '/lib/private/Security/RateLimiting/Limiter.php',
13741376
'OC\\Security\\SecureRandom' => $baseDir . '/lib/private/Security/SecureRandom.php',

lib/composer/composer/autoload_static.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
999999
'OC\\Core\\Migrations\\Version21000Date20210309185126' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20210309185126.php',
10001000
'OC\\Core\\Migrations\\Version21000Date20210309185127' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20210309185127.php',
10011001
'OC\\Core\\Migrations\\Version22000Date20210216080825' => __DIR__ . '/../../..' . '/core/Migrations/Version22000Date20210216080825.php',
1002+
'OC\\Core\\Migrations\\Version23000Date20210906132259' => __DIR__ . '/../../..' . '/core/Migrations/Version23000Date20210906132259.php',
10021003
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
10031004
'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php',
10041005
'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php',
@@ -1396,8 +1397,9 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
13961397
'OC\\Security\\IdentityProof\\Manager' => __DIR__ . '/../../..' . '/lib/private/Security/IdentityProof/Manager.php',
13971398
'OC\\Security\\IdentityProof\\Signer' => __DIR__ . '/../../..' . '/lib/private/Security/IdentityProof/Signer.php',
13981399
'OC\\Security\\Normalizer\\IpAddress' => __DIR__ . '/../../..' . '/lib/private/Security/Normalizer/IpAddress.php',
1400+
'OC\\Security\\RateLimiting\\Backend\\DatabaseBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php',
13991401
'OC\\Security\\RateLimiting\\Backend\\IBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/IBackend.php',
1400-
'OC\\Security\\RateLimiting\\Backend\\MemoryCache' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/MemoryCache.php',
1402+
'OC\\Security\\RateLimiting\\Backend\\MemoryCacheBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php',
14011403
'OC\\Security\\RateLimiting\\Exception\\RateLimitExceededException' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php',
14021404
'OC\\Security\\RateLimiting\\Limiter' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Limiter.php',
14031405
'OC\\Security\\SecureRandom' => __DIR__ . '/../../..' . '/lib/private/Security/SecureRandom.php',

lib/composer/composer/installed.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
<?php return array (
2-
'root' =>
2+
'root' =>
33
array (
44
'pretty_version' => 'dev-master',
55
'version' => 'dev-master',
6-
'aliases' =>
6+
'aliases' =>
77
array (
88
),
99
'reference' => '619b35b480a2d348436156a2a6144895b00b1e07',
1010
'name' => '__root__',
1111
),
12-
'versions' =>
12+
'versions' =>
1313
array (
14-
'__root__' =>
14+
'__root__' =>
1515
array (
1616
'pretty_version' => 'dev-master',
1717
'version' => 'dev-master',
18-
'aliases' =>
18+
'aliases' =>
1919
array (
2020
),
2121
'reference' => '619b35b480a2d348436156a2a6144895b00b1e07',
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright Copyright (c) 2021 Lukas Reschke <lukas@statuscode.ch>
7+
*
8+
* @author Lukas Reschke <lukas@statuscode.ch>
9+
*
10+
* @license GNU AGPL version 3 or any later version
11+
*
12+
* This program is free software: you can redistribute it and/or modify
13+
* it under the terms of the GNU Affero General Public License as
14+
* published by the Free Software Foundation, either version 3 of the
15+
* License, or (at your option) any later version.
16+
*
17+
* This program is distributed in the hope that it will be useful,
18+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
* GNU Affero General Public License for more details.
21+
*
22+
* You should have received a copy of the GNU Affero General Public License
23+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
24+
*
25+
*/
26+
namespace OC\Security\RateLimiting\Backend;
27+
28+
use OCP\AppFramework\Utility\ITimeFactory;
29+
use OCP\DB\QueryBuilder\IQueryBuilder;
30+
use OCP\IDBConnection;
31+
32+
class DatabaseBackend implements IBackend {
33+
private const TABLE_NAME = 'ratelimit_entries';
34+
35+
/** @var IDBConnection */
36+
private $dbConnection;
37+
/** @var ITimeFactory */
38+
private $timeFactory;
39+
40+
/**
41+
* @param IDBConnection $dbConnection
42+
* @param ITimeFactory $timeFactory
43+
*/
44+
public function __construct(
45+
IDBConnection $dbConnection,
46+
ITimeFactory $timeFactory
47+
) {
48+
$this->dbConnection = $dbConnection;
49+
$this->timeFactory = $timeFactory;
50+
}
51+
52+
/**
53+
* @param string $methodIdentifier
54+
* @param string $userIdentifier
55+
* @return string
56+
*/
57+
private function hash(string $methodIdentifier,
58+
string $userIdentifier): string {
59+
return hash('sha512', $methodIdentifier . $userIdentifier);
60+
}
61+
62+
/**
63+
* @param string $identifier
64+
* @param int $seconds
65+
* @return int
66+
* @throws \OCP\DB\Exception
67+
*/
68+
private function getExistingAttemptCount(
69+
string $identifier
70+
): int {
71+
$currentTime = $this->timeFactory->getDateTime();
72+
73+
$qb = $this->dbConnection->getQueryBuilder();
74+
$qb->delete(self::TABLE_NAME)
75+
->where(
76+
$qb->expr()->lte('delete_after', $qb->createNamedParameter($currentTime, IQueryBuilder::PARAM_DATE))
77+
)
78+
->executeStatement();
79+
80+
$qb = $this->dbConnection->getQueryBuilder();
81+
$qb->select($qb->func()->count())
82+
->from(self::TABLE_NAME)
83+
->where(
84+
$qb->expr()->eq('hash', $qb->createNamedParameter($identifier, IQueryBuilder::PARAM_STR))
85+
)
86+
->andWhere(
87+
$qb->expr()->gte('delete_after', $qb->createNamedParameter($currentTime, IQueryBuilder::PARAM_DATE))
88+
);
89+
90+
$cursor = $qb->executeQuery();
91+
$row = $cursor->fetchOne();
92+
$cursor->closeCursor();
93+
94+
return (int)$row;
95+
}
96+
97+
/**
98+
* {@inheritDoc}
99+
*/
100+
public function getAttempts(string $methodIdentifier,
101+
string $userIdentifier): int {
102+
$identifier = $this->hash($methodIdentifier, $userIdentifier);
103+
return $this->getExistingAttemptCount($identifier);
104+
}
105+
106+
/**
107+
* {@inheritDoc}
108+
*/
109+
public function registerAttempt(string $methodIdentifier,
110+
string $userIdentifier,
111+
int $period) {
112+
$identifier = $this->hash($methodIdentifier, $userIdentifier);
113+
$deleteAfter = $this->timeFactory->getDateTime()->add(new \DateInterval("PT{$period}S"));
114+
115+
$qb = $this->dbConnection->getQueryBuilder();
116+
117+
$qb->insert(self::TABLE_NAME)
118+
->values([
119+
'hash' => $qb->createNamedParameter($identifier, IQueryBuilder::PARAM_STR),
120+
'delete_after' => $qb->createNamedParameter($deleteAfter, IQueryBuilder::PARAM_DATE),
121+
])
122+
->executeStatement();
123+
}
124+
}

lib/private/Security/RateLimiting/Backend/IBackend.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,14 @@
3535
*/
3636
interface IBackend {
3737
/**
38-
* Gets the amount of attempts within the last specified seconds
38+
* Gets the number of attempts for the specified method
3939
*
4040
* @param string $methodIdentifier Identifier for the method
4141
* @param string $userIdentifier Identifier for the user
42-
* @param int $seconds Seconds to look back at
4342
* @return int
4443
*/
4544
public function getAttempts(string $methodIdentifier,
46-
string $userIdentifier,
47-
int $seconds): int;
45+
string $userIdentifier): int;
4846

4947
/**
5048
* Registers an attempt

lib/private/Security/RateLimiting/Backend/MemoryCache.php renamed to lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@
3333
use OCP\ICacheFactory;
3434

3535
/**
36-
* Class MemoryCache uses the configured distributed memory cache for storing
36+
* Class MemoryCacheBackend uses the configured distributed memory cache for storing
3737
* rate limiting data.
3838
*
3939
* @package OC\Security\RateLimiting\Backend
4040
*/
41-
class MemoryCache implements IBackend {
41+
class MemoryCacheBackend implements IBackend {
4242
/** @var ICache */
4343
private $cache;
4444
/** @var ITimeFactory */
@@ -86,16 +86,14 @@ private function getExistingAttempts(string $identifier): array {
8686
* {@inheritDoc}
8787
*/
8888
public function getAttempts(string $methodIdentifier,
89-
string $userIdentifier,
90-
int $seconds): int {
89+
string $userIdentifier): int {
9190
$identifier = $this->hash($methodIdentifier, $userIdentifier);
9291
$existingAttempts = $this->getExistingAttempts($identifier);
9392

9493
$count = 0;
9594
$currentTime = $this->timeFactory->getTime();
96-
/** @var array $existingAttempts */
97-
foreach ($existingAttempts as $attempt) {
98-
if (($attempt + $seconds) > $currentTime) {
95+
foreach ($existingAttempts as $expirationTime) {
96+
if ($expirationTime > $currentTime) {
9997
$count++;
10098
}
10199
}
@@ -113,16 +111,16 @@ public function registerAttempt(string $methodIdentifier,
113111
$existingAttempts = $this->getExistingAttempts($identifier);
114112
$currentTime = $this->timeFactory->getTime();
115113

116-
// Unset all attempts older than $period
117-
foreach ($existingAttempts as $key => $attempt) {
118-
if (($attempt + $period) < $currentTime) {
114+
// Unset all attempts that are already expired
115+
foreach ($existingAttempts as $key => $expirationTime) {
116+
if ($expirationTime < $currentTime) {
119117
unset($existingAttempts[$key]);
120118
}
121119
}
122120
$existingAttempts = array_values($existingAttempts);
123121

124122
// Store the new attempt
125-
$existingAttempts[] = (string)$currentTime;
123+
$existingAttempts[] = (string)($currentTime + $period);
126124
$this->cache->set($identifier, json_encode($existingAttempts));
127125
}
128126
}

lib/private/Security/RateLimiting/Limiter.php

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,17 @@
2929
use OC\Security\Normalizer\IpAddress;
3030
use OC\Security\RateLimiting\Backend\IBackend;
3131
use OC\Security\RateLimiting\Exception\RateLimitExceededException;
32-
use OCP\AppFramework\Utility\ITimeFactory;
3332
use OCP\IUser;
3433

3534
class Limiter {
3635
/** @var IBackend */
3736
private $backend;
38-
/** @var ITimeFactory */
39-
private $timeFactory;
4037

4138
/**
42-
* @param ITimeFactory $timeFactory
4339
* @param IBackend $backend
4440
*/
45-
public function __construct(ITimeFactory $timeFactory,
46-
IBackend $backend) {
41+
public function __construct(IBackend $backend) {
4742
$this->backend = $backend;
48-
$this->timeFactory = $timeFactory;
4943
}
5044

5145
/**
@@ -59,12 +53,12 @@ private function register(string $methodIdentifier,
5953
string $userIdentifier,
6054
int $period,
6155
int $limit): void {
62-
$existingAttempts = $this->backend->getAttempts($methodIdentifier, $userIdentifier, $period);
56+
$existingAttempts = $this->backend->getAttempts($methodIdentifier, $userIdentifier);
6357
if ($existingAttempts >= $limit) {
6458
throw new RateLimitExceededException();
6559
}
6660

67-
$this->backend->registerAttempt($methodIdentifier, $userIdentifier, $this->timeFactory->getTime());
61+
$this->backend->registerAttempt($methodIdentifier, $userIdentifier, $period);
6862
}
6963

7064
/**

lib/private/Server.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -787,10 +787,20 @@ public function __construct($webRoot, \OC\Config $config) {
787787
$this->registerDeprecatedAlias('Search', ISearch::class);
788788

789789
$this->registerService(\OC\Security\RateLimiting\Backend\IBackend::class, function ($c) {
790-
return new \OC\Security\RateLimiting\Backend\MemoryCache(
791-
$this->get(ICacheFactory::class),
792-
new \OC\AppFramework\Utility\TimeFactory()
793-
);
790+
$cacheFactory = $c->get(ICacheFactory::class);
791+
if ($cacheFactory->isAvailable()) {
792+
$backend = new \OC\Security\RateLimiting\Backend\MemoryCacheBackend(
793+
$this->get(ICacheFactory::class),
794+
new \OC\AppFramework\Utility\TimeFactory()
795+
);
796+
} else {
797+
$backend = new \OC\Security\RateLimiting\Backend\DatabaseBackend(
798+
$c->get(IDBConnection::class),
799+
new \OC\AppFramework\Utility\TimeFactory()
800+
);
801+
}
802+
803+
return $backend;
794804
});
795805

796806
$this->registerAlias(\OCP\Security\ISecureRandom::class, SecureRandom::class);

0 commit comments

Comments
 (0)