fix(07): WR-02 move rate limit check before validation in LoginPostHandler

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-05-15 18:55:27 +02:00
parent b61f36f17e
commit ab12bf0962
No known key found for this signature in database

View file

@ -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)