Summary
Outstanding password-reset tokens survive a password change, and reset-token expiry is checked only in Go, not in the SQL write.
Location (origin/main 496d9d7)
internal/auth/service_password.go:231 ChangePassword and internal/auth/store_postgres_credentials.go:11 UpdateUserCredentials write the new password without clearing password_reset_token / password_reset_expiry.
internal/auth/store_postgres_credentials.go:29-34 CompletePasswordReset matches on token and password hash but not on expiry.
- Expiry is checked only in
validateResetToken (internal/auth/service_password.go:503).
Failure scenario
- A user requests a reset and the token is issued.
- The user changes their password through the profile flow. The token stays valid.
- The holder of the stale token completes the reset and overwrites the newer password.
The expiry gap is a window of about one bcrypt hash, not exploitable in practice.
Context
Found in the #496 review. Related: #398, #421, #493.
Acceptance
ChangePassword and UpdateUserCredentials clear outstanding reset tokens, CompletePasswordReset enforces password_reset_expiry > NOW() in SQL, with tests for both.
Summary
Outstanding password-reset tokens survive a password change, and reset-token expiry is checked only in Go, not in the SQL write.
Location (origin/main 496d9d7)
internal/auth/service_password.go:231ChangePasswordandinternal/auth/store_postgres_credentials.go:11UpdateUserCredentialswrite the new password without clearingpassword_reset_token/password_reset_expiry.internal/auth/store_postgres_credentials.go:29-34CompletePasswordResetmatches on token and password hash but not on expiry.validateResetToken(internal/auth/service_password.go:503).Failure scenario
The expiry gap is a window of about one bcrypt hash, not exploitable in practice.
Context
Found in the #496 review. Related: #398, #421, #493.
Acceptance
ChangePasswordandUpdateUserCredentialsclear outstanding reset tokens,CompletePasswordResetenforcespassword_reset_expiry > NOW()in SQL, with tests for both.