mirror of
https://github.com/nextcloud/all-in-one.git
synced 2026-05-21 10:50:10 +00:00
feat: add proper rate limiting for login endpoints
- Add RateLimiter class using APCu for per-IP tracking - Limit failed login attempts to 10 per 15-minute window - Apply rate limiting in TryLogin (POST /api/auth/login) and GetTryLogin (GET /api/auth/getlogin) - Return 429 Too Many Requests with Retry-After header when limit exceeded - Reset counter on successful login - Register RateLimiter in DependencyInjection container Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/1ce6e415-a2b7-478c-955f-72f582c157c4 Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
15ae285d9f
commit
a466cd4284
44
php/src/Auth/RateLimiter.php
Normal file
44
php/src/Auth/RateLimiter.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace AIO\Auth;
|
||||||
|
|
||||||
|
class RateLimiter {
|
||||||
|
public const int MAX_ATTEMPTS = 10;
|
||||||
|
public const int WINDOW_SECONDS = 900; // 15 minutes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when the IP has exceeded the maximum number of failed login
|
||||||
|
* attempts within the current time window and should be blocked.
|
||||||
|
*/
|
||||||
|
public function isLimitReached(string $ip): bool {
|
||||||
|
$attempts = apcu_fetch($this->getKey($ip));
|
||||||
|
return $attempts !== false && (int)$attempts >= self::MAX_ATTEMPTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a failed login attempt for the given IP.
|
||||||
|
* Uses a 15-minute sliding window: the first failure starts the window and
|
||||||
|
* subsequent failures within that window are counted together.
|
||||||
|
*/
|
||||||
|
public function recordFailedAttempt(string $ip): void {
|
||||||
|
$key = $this->getKey($ip);
|
||||||
|
// apcu_add only stores when the key does not yet exist.
|
||||||
|
// If it already exists (returns false), we increment the existing counter.
|
||||||
|
if (!apcu_add($key, 1, self::WINDOW_SECONDS)) {
|
||||||
|
apcu_inc($key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the failed-attempt counter for the given IP, e.g. after a
|
||||||
|
* successful login.
|
||||||
|
*/
|
||||||
|
public function resetAttempts(string $ip): void {
|
||||||
|
apcu_delete($this->getKey($ip));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getKey(string $ip): string {
|
||||||
|
return 'login_attempts_' . hash('sha256', $ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
namespace AIO\Controller;
|
namespace AIO\Controller;
|
||||||
|
|
||||||
use AIO\Auth\AuthManager;
|
use AIO\Auth\AuthManager;
|
||||||
|
use AIO\Auth\RateLimiter;
|
||||||
use AIO\Container\Container;
|
use AIO\Container\Container;
|
||||||
use AIO\ContainerDefinitionFetcher;
|
use AIO\ContainerDefinitionFetcher;
|
||||||
use AIO\Docker\DockerActionManager;
|
use AIO\Docker\DockerActionManager;
|
||||||
@@ -14,20 +15,33 @@ readonly class LoginController {
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private AuthManager $authManager,
|
private AuthManager $authManager,
|
||||||
private DockerActionManager $dockerActionManager,
|
private DockerActionManager $dockerActionManager,
|
||||||
|
private RateLimiter $rateLimiter,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function TryLogin(Request $request, Response $response, array $args) : Response {
|
public function TryLogin(Request $request, Response $response, array $args) : Response {
|
||||||
|
$ip = (string)($request->getServerParams()['REMOTE_ADDR'] ?? '');
|
||||||
|
|
||||||
|
if ($this->rateLimiter->isLimitReached($ip)) {
|
||||||
|
$response->getBody()->write("Too many failed login attempts. Please try again later.");
|
||||||
|
return $response
|
||||||
|
->withHeader('Retry-After', (string)RateLimiter::WINDOW_SECONDS)
|
||||||
|
->withStatus(429);
|
||||||
|
}
|
||||||
|
|
||||||
if (!$this->dockerActionManager->isLoginAllowed()) {
|
if (!$this->dockerActionManager->isLoginAllowed()) {
|
||||||
$response->getBody()->write("The login is blocked since Nextcloud is running.");
|
$response->getBody()->write("The login is blocked since Nextcloud is running.");
|
||||||
return $response->withHeader('Location', '.')->withStatus(422);
|
return $response->withHeader('Location', '.')->withStatus(422);
|
||||||
}
|
}
|
||||||
$password = $request->getParsedBody()['password'] ?? '';
|
$password = $request->getParsedBody()['password'] ?? '';
|
||||||
if($this->authManager->CheckCredentials($password)) {
|
if($this->authManager->CheckCredentials($password)) {
|
||||||
|
$this->rateLimiter->resetAttempts($ip);
|
||||||
$this->authManager->SetAuthState(true);
|
$this->authManager->SetAuthState(true);
|
||||||
return $response->withHeader('Location', '.')->withStatus(201);
|
return $response->withHeader('Location', '.')->withStatus(201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->rateLimiter->recordFailedAttempt($ip);
|
||||||
|
|
||||||
// Punish failed auth attempts with a delay, as a very simple means against bots.
|
// Punish failed auth attempts with a delay, as a very simple means against bots.
|
||||||
sleep(5);
|
sleep(5);
|
||||||
|
|
||||||
@@ -36,12 +50,23 @@ readonly class LoginController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function GetTryLogin(Request $request, Response $response, array $args) : Response {
|
public function GetTryLogin(Request $request, Response $response, array $args) : Response {
|
||||||
|
$ip = (string)($request->getServerParams()['REMOTE_ADDR'] ?? '');
|
||||||
|
|
||||||
|
if ($this->rateLimiter->isLimitReached($ip)) {
|
||||||
|
return $response
|
||||||
|
->withHeader('Retry-After', (string)RateLimiter::WINDOW_SECONDS)
|
||||||
|
->withStatus(429);
|
||||||
|
}
|
||||||
|
|
||||||
$token = $request->getQueryParams()['token'] ?? '';
|
$token = $request->getQueryParams()['token'] ?? '';
|
||||||
if($this->authManager->CheckToken($token)) {
|
if($this->authManager->CheckToken($token)) {
|
||||||
|
$this->rateLimiter->resetAttempts($ip);
|
||||||
$this->authManager->SetAuthState(true);
|
$this->authManager->SetAuthState(true);
|
||||||
return $response->withHeader('Location', '../..')->withStatus(302);
|
return $response->withHeader('Location', '../..')->withStatus(302);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->rateLimiter->recordFailedAttempt($ip);
|
||||||
|
|
||||||
// Punish failed auth attempts with a delay, as a very simple means against bots.
|
// Punish failed auth attempts with a delay, as a very simple means against bots.
|
||||||
sleep(5);
|
sleep(5);
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ class DependencyInjection
|
|||||||
\AIO\Auth\AuthManager::class,
|
\AIO\Auth\AuthManager::class,
|
||||||
new \AIO\Auth\AuthManager($container->get(\AIO\Data\ConfigurationManager::class))
|
new \AIO\Auth\AuthManager($container->get(\AIO\Data\ConfigurationManager::class))
|
||||||
);
|
);
|
||||||
|
$container->set(
|
||||||
|
\AIO\Auth\RateLimiter::class,
|
||||||
|
new \AIO\Auth\RateLimiter()
|
||||||
|
);
|
||||||
$container->set(
|
$container->set(
|
||||||
\AIO\Data\Setup::class,
|
\AIO\Data\Setup::class,
|
||||||
new \AIO\Data\Setup(
|
new \AIO\Data\Setup(
|
||||||
|
|||||||
Reference in New Issue
Block a user