fix: changing a password spends the reset links outstanding for that user - #916
Merged
Merged
Conversation
…user toggleUsedByHash() consumes only the token being redeemed, called from one place, and nothing consumed the rest — no method existed that could: the repository offered getAttemptsByUserId, add, toggleUsedByHash and getUserIdForHash, and nothing else. requestForUserId() allows up to MAX_PASS_RECOVER_LIMIT = 3 outstanding tokens per user, each valid for an hour. So somebody who obtained a reset link they should not have kept a working 'set this account's password' capability, and the obvious response did not revoke it: neither the user changing their own password nor an administrator resetting it touched the outstanding tokens. toggleUsedByUserId() spends every unused token for a user, called from User::updatePass() after a successful write — the one method both the administrator's edit and the completion of a reset go through, so a third caller cannot forget. updateOnLogin() (LDAP sync) and migrateUserPassById() are deliberately left alone: the first belongs to directory users, whose password lives in the directory and who have no local reset flow, and the second re-hashes the same password rather than changing it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
UserPassRecover::toggleUsedByHash()consumes the one token being redeemed, and it is called fromexactly one place —
SaveResetController, on the hash the caller supplied. Nothing consumed therest, and no method existed that could: the repository offered
getAttemptsByUserId,add,toggleUsedByHashandgetUserIdForHash, and nothing else.requestForUserId()allows up toMAX_PASS_RECOVER_LIMIT = 3outstanding tokens per user, eachvalid for an hour. So somebody who obtained a reset link they should not have — a forwarded mail, a
shared machine, a mailbox someone else can read — kept a working set this account's password
capability, and the obvious response did not revoke it: neither the user changing their own
password nor an administrator resetting it touched the outstanding tokens.
The change
toggleUsedByUserId()spends every unused token for a user, andUser::updatePass()calls it aftera successful write.
updatePass()is the one method both doors go through —User\SaveEditPassController(anadministrator, or the user changing their own) and
UserPassReset\SaveResetController(completing areset). Putting it there rather than in the two controllers means a third caller cannot forget.
No date bound on the update, unlike
toggleUsedByHash(): an expired token is already refused bygetUserIdForHash(), and marking it used costs nothing and leaves less to reason about.Deliberately not included
User::updateOnLogin()(LDAP sync) andUserPass::migrateUserPassById()also write thepasscolumn and are left alone. The first belongs to directory users, whose password lives in the
directory and who have no local reset flow; the second re-hashes the same password rather than
changing it. Spending tokens there would be revoking a link on an event the user did not cause.
Tests
Two: a successful change spends the tokens, and a change that affected no row spends none — the
second because the first would otherwise also be satisfied by revoking on a failed write.
Mutation-verified: reverting
User::updatePass()fails the first and leaves the second passing,which is right, since it asserts an absence.
Also worth recording: probing the real container for the new dependency first answered
ArgumentCountError: … 3 passed … and exactly 4 expectedfromvar/cache/CompiledContainerWeb40021031301.php. That is the documented stale-compiled-containertrap, not a wiring fault — after clearing
var/cache, the container handsUserServicea realUserPassRecover.