Fix SEC-02: normalize lockout response
- REST login: change locked account response from HTTP 429 "account_locked" to HTTP 401 "invalid credentials" - gRPC login: change from ResourceExhausted to Unauthenticated with "invalid credentials" message - UI login: change from "account temporarily locked" to "invalid credentials" - REST password-change endpoint: same normalization - Audit logs still record "account_locked" internally - Added tests in all three layers verifying locked-account responses are indistinguishable from wrong-password responses Security: lockout responses now return identical status codes and messages as wrong-password failures across REST, gRPC, and UI, preventing user-enumeration via lockout differentiation. Internal audit logging of lockout events is preserved for operational use. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -80,7 +80,9 @@ func (u *UIServer) handleLoginPost(w http.ResponseWriter, r *http.Request) {
|
||||
if locked {
|
||||
_, _ = auth.VerifyPassword("dummy", u.dummyHash())
|
||||
u.writeAudit(r, model.EventLoginFail, &acct.ID, nil, `{"reason":"account_locked"}`)
|
||||
u.render(w, "login", LoginData{Error: "account temporarily locked, please try again later"})
|
||||
// Security: return the same "invalid credentials" as wrong-password
|
||||
// to prevent user-enumeration via lockout differentiation (SEC-02).
|
||||
u.render(w, "login", LoginData{Error: "invalid credentials"})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user