From ab12bf096222e8a87f4e322d0f531e307eab49cf Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Fri, 15 May 2026 18:55:27 +0200 Subject: [PATCH] fix(07): WR-02 move rate limit check before validation in LoginPostHandler Co-Authored-By: Claude Sonnet 4.6 (1M context) --- backend/internal/web/handlers_auth.go | 33 +++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/backend/internal/web/handlers_auth.go b/backend/internal/web/handlers_auth.go index 1d3eef6..083bd0d 100644 --- a/backend/internal/web/handlers_auth.go +++ b/backend/internal/web/handlers_auth.go @@ -165,8 +165,8 @@ func LoginPageHandler() http.HandlerFunc { } // LoginPostHandler handles POST /login: -// 1. Validate email + password format (specific errors per D-20/D-25) -// 2. Rate-limit check before any expensive work (D-16, D-18; gates DoS T-2-14) +// 1. Rate-limit check before any other work — even invalid inputs consume tokens (D-16, D-18; gates DoS T-2-14) +// 2. Validate email + password format (specific errors per D-20/D-25) // 3. Look up user by email // 4. Verify argon2id password hash // 5. Rotate session (D-10 fixation defense) @@ -189,14 +189,27 @@ func LoginPostHandler(deps AuthDeps) http.HandlerFunc { email := strings.TrimSpace(r.PostFormValue("email")) password := r.PostFormValue("password") + // 2. Rate-limit check FIRST — even on invalid input, to prevent enumeration + // via timing differences on malformed payloads (D-16, T-2-14). + // Key uses lowercased raw email + IP so malformed inputs consume rate tokens. + // Limiter may be nil in tests that don't exercise rate-limiting. + ip := clientIP(r) + key := strings.ToLower(email) + ":" + ip + if deps.Limiter != nil && !deps.Limiter.Allow(key) { + var rateLimitErrs templates.LoginErrors + rateLimitErrs.General = "Too many attempts. Try again in a minute." + renderLoginError(w, r, templates.LoginForm{Email: email}, rateLimitErrs, http.StatusTooManyRequests) + return + } + var errs templates.LoginErrors - // 2. Validate email (specific error — D-20: validation errors ARE specific). + // 3. Validate email (specific error — D-20: validation errors ARE specific). if _, err := mail.ParseAddress(email); err != nil { errs.Email = "Enter a valid email address" } - // 3. Validate password length BEFORE calling Verify (DoS guard T-2-14, D-25). + // 4. Validate password length BEFORE calling Verify (DoS guard T-2-14, D-25). if len(password) < 12 { errs.Password = "Password must be at least 12 characters" } else if len(password) > 128 { @@ -208,18 +221,8 @@ func LoginPostHandler(deps AuthDeps) http.HandlerFunc { return } - // 4. Normalize email for rate-limit key and DB lookup. + // 5. Normalize email for DB lookup. normalized := strings.ToLower(email) - ip := clientIP(r) - key := normalized + ":" + ip - - // 5. Rate-limit check BEFORE user lookup and argon2 work (D-16, T-2-14). - // Limiter may be nil in tests that don't exercise rate-limiting. - if deps.Limiter != nil && !deps.Limiter.Allow(key) { - errs.General = "Too many attempts. Try again in a minute." - renderLoginError(w, r, templates.LoginForm{Email: email}, errs, http.StatusTooManyRequests) - return - } // 6. Look up user by email. user, err := deps.Queries.GetUserByEmail(ctx, normalized)