fix(auth): longer IDs and invite codes, register limits, single-use invites (FLAW-32, FLAW-39, FLAW-40)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m2s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m2s
- NewID uses 96 random bits; invite codes get their own 100 bits in Base32. - Registration and forgot-password are limited per address via loginGuard. - The invite is claimed atomically before the account is created and released again when the account cannot be created. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1af3a6e90a
commit
3709282a4f
11 files changed
+220
-15
No files matched your search
@@ -135,6 +135,46 @@ func (g *loginGuard) cleanupLocked(now time.Time) {
|
||||
}
|
||||
}
|
||||
|
||||
// Other endpoints that can be abused in bulk are limited per address in the same buckets (FLAW-39).
|
||||
// Every request counts here, not only failures: registration (also guessing invite codes) and
|
||||
// "forgot password" (mail flood to many addresses).
|
||||
const (
|
||||
limitRegisterPerIP = 20
|
||||
limitForgotPerIP = 10
|
||||
)
|
||||
|
||||
// throttle counts one request of this kind from the visitor's address and returns how long further
|
||||
// requests are refused (0 = this one may go ahead). Unknown addresses are not limited.
|
||||
func (g *loginGuard) throttle(kind string, ip clientIPInfo, limit int) time.Duration {
|
||||
if !g.enabled || ip.viaProxyWithoutHeader || ip.ip == "" {
|
||||
return 0
|
||||
}
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
now := g.now()
|
||||
g.cleanupLocked(now)
|
||||
key := kind + ":" + ip.ip
|
||||
b := g.buckets[key]
|
||||
if b != nil && b.lockedUntil.After(now) {
|
||||
return b.lockedUntil.Sub(now)
|
||||
}
|
||||
if b == nil || now.Sub(b.windowStart) > loginLockWindow {
|
||||
b = &loginBucket{windowStart: now}
|
||||
g.buckets[key] = b
|
||||
}
|
||||
b.count++
|
||||
if b.count >= limit {
|
||||
b.lockedUntil = now.Add(loginLockWindow)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func writeThrottled(w http.ResponseWriter, wait time.Duration) {
|
||||
w.Header().Set("Retry-After", strconv.Itoa(int(math.Ceil(wait.Seconds()))))
|
||||
writeError(w, http.StatusTooManyRequests,
|
||||
fmt.Sprintf("Zu viele Anfragen von dieser Adresse. Bitte versuche es in %d Minuten erneut.", int(math.Ceil(wait.Minutes()))))
|
||||
}
|
||||
|
||||
func writeLoginLocked(w http.ResponseWriter, wait time.Duration) {
|
||||
minutes := int(math.Ceil(wait.Minutes()))
|
||||
w.Header().Set("Retry-After", strconv.Itoa(int(math.Ceil(wait.Seconds()))))
|
||||
|
||||
Reference in new issue
Block a user