From dc32dd2954fd1816302678320b45f43e42662a44 Mon Sep 17 00:00:00 2001 From: Pablo Zmdl Date: Wed, 1 Apr 2026 00:02:00 +0200 Subject: [PATCH] Throttle login attempts to 5 failures per 5 minutes AI-assistant: Copilot v1.0.7 (Claude Opus 4.6) Signed-off-by: Pablo Zmdl --- php/public/forms.js | 3 +++ php/src/Controller/LoginController.php | 29 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/php/public/forms.js b/php/public/forms.js index 6b982b0d..31594c62 100644 --- a/php/public/forms.js +++ b/php/public/forms.js @@ -29,6 +29,9 @@ function showPassword(id) { const xhr = e.target; if (xhr.status === 201) { window.location.replace(xhr.getResponseHeader('Location')); + } else if ([422, 429].includes(xhr.status)) { + disableSpinner() + showError(xhr.response); } else if (xhr.status === 422) { disableSpinner() showError(xhr.response); diff --git a/php/src/Controller/LoginController.php b/php/src/Controller/LoginController.php index a90bde26..471a89af 100644 --- a/php/src/Controller/LoginController.php +++ b/php/src/Controller/LoginController.php @@ -11,23 +11,52 @@ use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; readonly class LoginController { + private const int MAX_LOGIN_ATTEMPTS_PER_TTL = 5; + private const int LOGIN_COUNTER_TTL = 300; + private const string RATE_LIMIT_CACHE_KEY = 'login_failed_attempts'; + public function __construct( private AuthManager $authManager, private DockerActionManager $dockerActionManager, ) { } + private function getFailedLoginCount() : int { + $count = apcu_fetch(self::RATE_LIMIT_CACHE_KEY); + return $count !== false ? (int)$count : 0; + } + + private function incrementFailedLoginCount() : void { + if (!apcu_exists(self::RATE_LIMIT_CACHE_KEY)) { + apcu_store(self::RATE_LIMIT_CACHE_KEY, 1, self::LOGIN_COUNTER_TTL); + } else { + apcu_inc(self::RATE_LIMIT_CACHE_KEY); + } + } + + private function resetFailedLoginCount() : void { + apcu_delete(self::RATE_LIMIT_CACHE_KEY); + } + public function TryLogin(Request $request, Response $response, array $args) : Response { if (!$this->dockerActionManager->isLoginAllowed()) { $response->getBody()->write("The login is blocked since Nextcloud is running."); return $response->withHeader('Location', '.')->withStatus(422); } + + if ($this->getFailedLoginCount() >= self::MAX_LOGIN_ATTEMPTS_PER_TTL) { + $response->getBody()->write("Too many failed login attempts. Please try again in some minutes."); + return $response->withHeader('Location', '.')->withStatus(429); + } + $password = $request->getParsedBody()['password'] ?? ''; if($this->authManager->CheckCredentials($password)) { + $this->resetFailedLoginCount(); $this->authManager->SetAuthState(true); return $response->withHeader('Location', '.')->withStatus(201); } + $this->incrementFailedLoginCount(); $response->getBody()->write("The password is incorrect."); return $response->withHeader('Location', '.')->withStatus(422); }