From a466cd42843d1123ac56c2c360b4e747ff7b8fc3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 10:10:40 +0000 Subject: [PATCH] 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> --- php/src/Auth/RateLimiter.php | 44 ++++++++++++++++++++++++++ php/src/Controller/LoginController.php | 25 +++++++++++++++ php/src/DependencyInjection.php | 4 +++ 3 files changed, 73 insertions(+) create mode 100644 php/src/Auth/RateLimiter.php diff --git a/php/src/Auth/RateLimiter.php b/php/src/Auth/RateLimiter.php new file mode 100644 index 00000000..57e02de0 --- /dev/null +++ b/php/src/Auth/RateLimiter.php @@ -0,0 +1,44 @@ +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); + } +} diff --git a/php/src/Controller/LoginController.php b/php/src/Controller/LoginController.php index d37a2210..e3279579 100644 --- a/php/src/Controller/LoginController.php +++ b/php/src/Controller/LoginController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace AIO\Controller; use AIO\Auth\AuthManager; +use AIO\Auth\RateLimiter; use AIO\Container\Container; use AIO\ContainerDefinitionFetcher; use AIO\Docker\DockerActionManager; @@ -14,20 +15,33 @@ readonly class LoginController { public function __construct( private AuthManager $authManager, private DockerActionManager $dockerActionManager, + private RateLimiter $rateLimiter, ) { } 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()) { $response->getBody()->write("The login is blocked since Nextcloud is running."); return $response->withHeader('Location', '.')->withStatus(422); } $password = $request->getParsedBody()['password'] ?? ''; if($this->authManager->CheckCredentials($password)) { + $this->rateLimiter->resetAttempts($ip); $this->authManager->SetAuthState(true); return $response->withHeader('Location', '.')->withStatus(201); } + $this->rateLimiter->recordFailedAttempt($ip); + // Punish failed auth attempts with a delay, as a very simple means against bots. sleep(5); @@ -36,12 +50,23 @@ readonly class LoginController { } 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'] ?? ''; if($this->authManager->CheckToken($token)) { + $this->rateLimiter->resetAttempts($ip); $this->authManager->SetAuthState(true); return $response->withHeader('Location', '../..')->withStatus(302); } + $this->rateLimiter->recordFailedAttempt($ip); + // Punish failed auth attempts with a delay, as a very simple means against bots. sleep(5); diff --git a/php/src/DependencyInjection.php b/php/src/DependencyInjection.php index a7035a96..0c89b87d 100644 --- a/php/src/DependencyInjection.php +++ b/php/src/DependencyInjection.php @@ -43,6 +43,10 @@ class DependencyInjection \AIO\Auth\AuthManager::class, new \AIO\Auth\AuthManager($container->get(\AIO\Data\ConfigurationManager::class)) ); + $container->set( + \AIO\Auth\RateLimiter::class, + new \AIO\Auth\RateLimiter() + ); $container->set( \AIO\Data\Setup::class, new \AIO\Data\Setup(