feat(auth): lock logins after repeated failures, trust proxies explicitly (FLAW-09)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s

Failed logins are counted per account+address (3), per account (20) and
per address (30) within 15 minutes; a full bucket answers 429 with
Retry-After for 15 minutes. Locking the account+address bucket first
means an attacker cannot lock the owner out from elsewhere. Unknown
names count like accounts. Success clears the account buckets, a
password reset (mail or admin) lifts the lock. LOGIN_PROTECTION=false
switches it off.

chi's RealIP believed X-Forwarded-For from anyone. The client address
now comes from the headers only when the connection is from a trusted
proxy: TRUSTED_PROXIES=private (default: loopback, Docker, LAN), all
(old behaviour), none, or CIDRs. A proxy that sends no client address
is logged once and per-address limits are skipped for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-29 14:26:02 +01:00
1 parent 6b9eb62caf
commit 11c14fa9ff
12 files changed
+503 -30

No files matched your search

+4 -2
View File
@@ -29,7 +29,8 @@ type Server struct {
leases *collab.LeaseManager // block leases and open sessions of notes
notifier *collab.Notifier // wakes long-poll requests on note changes
demo demoState // periodic reset in demo mode (demo.go)
demo demoState // periodic reset in demo mode (demo.go)
loginGuard *loginGuard // brute-force protection (loginguard.go)
}
func NewServer(cfg *config.Config, store *storage.Store, wsHub *ws.Hub, staticFS fs.FS) *Server {
@@ -41,6 +42,7 @@ func NewServer(cfg *config.Config, store *storage.Store, wsHub *ws.Hub, staticFS
leases: collab.NewLeaseManager(nil),
notifier: collab.NewNotifier(),
}
srv.loginGuard = newLoginGuard(!cfg.DisableLoginProtection)
wsHub.SetRoomAuthorizer(srv.canJoinRoom)
// Next occurrences of repeating tasks appear live on every connected client
store.OnTaskCreated = func(th *domain.Thought) {
@@ -74,7 +76,7 @@ func (s *Server) taskAudience(th *domain.Thought) []string {
func (s *Server) Router() http.Handler {
r := chi.NewRouter()
r.Use(middleware.RealIP)
r.Use(s.clientIPMiddleware)
r.Use(s.demoResetGate)
r.Use(middleware.Recoverer)
r.Use(s.apiLoggingMiddleware)