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

- 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:
sandrodachandClaude Opus 5.5 committed 2026-10-29 19:28:09 +01:00
1 parent 1af3a6e90a
commit 3709282a4f
11 files changed
+220 -15

No files matched your search

+40
View File
@@ -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()))))