[Server] Fix rate limiter: disable semantics, draining window, bounded keys, pre-work login gate

- recordAttempt()/isBlocked() return false when maxAttempts <= 0 or windowSeconds <= 0, so the documented 'set to 0 to disable' actually disables instead of blocking every address permanently
- Keys at their limit are rejected without recording, so repeated attempts at the attacker's pace cannot keep sliding the window and holding the lockout open; a block now clears once the recorded attempts age out of windowSeconds
- Attempts map is bounded: an opportunistic prune (every 60s) erases keys whose attempts have fully aged out
- Login is throttled before the work: isLoginRateLimited() (a new virtual on Server, backed by isBlocked()) gates loginUser(), so a locked-out address no longer burns a database round trip and password verification per attempt
- recordAttemptAt()/isBlockedAt() time seams make the sliding-window behaviour deterministically testable; tests rewritten for the real semantics (no reliance on the maxAttempts=0 bug, no sleeps)
This commit is contained in:
Lukas Brübach 2026-08-30 23:32:32 +02:00
parent b2f63255f0
commit 152a74d3f4
7 changed files with 202 additions and 29 deletions

View file

@ -180,6 +180,16 @@ public:
{
return false;
}
/**
* @brief True if the given address is already locked out of logging in.
*
* Consulted before any authentication work (database round trip, password
* verification) so a blocked address cannot burn server CPU per attempt.
*/
virtual bool isLoginRateLimited(const QString & /*ipAddress*/)
{
return false;
}
/** @brief Clear any failed-login lockout for the given address, e.g. after a successful login. */
virtual void clearFailedLogins(const QString & /*ipAddress*/)
{