Cap concurrent password checks and count attempts before checking
Every argon2 run takes 64 MiB and nothing limited how many ran at once, so parallel sign-in attempts could run the server out of memory (8 at once used about 600 MB). At most two now run at once; at most 16 sign-ins wait for one, more get HTTP 429. 30 parallel sign-ins peaked at 275 MB. A sign-in attempt now counts toward the lockout before its password is checked, so parallel attempts cannot get past it; a right password takes its own attempt back. IPv6 addresses are locked out by /64.
This commit is contained in:
@@ -236,23 +236,34 @@ func newMFAState() mfaState {
|
||||
|
||||
var errBadTicket = errors.New("the sign-in expired; enter your password again")
|
||||
|
||||
// failLocked counts a failed attempt from ip toward the lockout. a.mu must
|
||||
// be held.
|
||||
func (a *Auth) failLocked(ip string) {
|
||||
f := a.fails[ip]
|
||||
// failLocked counts a failed attempt from ip toward the lockout and returns
|
||||
// a function that takes it back, for an attempt counted before it was
|
||||
// checked. a.mu must be held, also when calling undo.
|
||||
func (a *Auth) failLocked(ip string) (undo func()) {
|
||||
key := lockKey(ip)
|
||||
f := a.fails[key]
|
||||
if f == nil {
|
||||
f = &failState{}
|
||||
a.fails[ip] = f
|
||||
a.fails[key] = f
|
||||
}
|
||||
f.count++
|
||||
if f.count >= maxFailures {
|
||||
locked := f.count >= maxFailures
|
||||
if locked {
|
||||
f.count = 0
|
||||
f.until = time.Now().Add(lockoutTime)
|
||||
}
|
||||
return func() {
|
||||
switch {
|
||||
case locked:
|
||||
f.count, f.until = maxFailures-1, time.Time{}
|
||||
case f.count > 0:
|
||||
f.count--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Auth) lockedLocked(ip string) bool {
|
||||
f := a.fails[ip]
|
||||
f := a.fails[lockKey(ip)]
|
||||
return f != nil && time.Now().Before(f.until)
|
||||
}
|
||||
|
||||
@@ -302,7 +313,7 @@ func (a *Auth) finishSignIn(u *User, ip string) string {
|
||||
cfg := a.store.Get()
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
delete(a.fails, ip)
|
||||
delete(a.fails, lockKey(ip))
|
||||
a.logins[u.ID] = tokenUse{At: time.Now(), IP: ip}
|
||||
return a.newSessionLocked(cfg, u, sessionInfo{Started: time.Now(), IP: ip})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user