Skip to content

fix: changing a password spends the reset links outstanding for that user - #916

Merged
blaipr merged 1 commit into
mainfrom
fix/changing-a-password-spends-the-reset-tokens
Sep 7, 2026
Merged

fix: changing a password spends the reset links outstanding for that user#916
blaipr merged 1 commit into
mainfrom
fix/changing-a-password-spends-the-reset-tokens

Conversation

@blaipr

@blaipr blaipr commented Sep 7, 2026

Copy link
Copy Markdown
Member

UserPassRecover::toggleUsedByHash() consumes the one token being redeemed, and it is called from
exactly one place — SaveResetController, on the hash the caller supplied. Nothing consumed the
rest, and 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 — 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, and User::updatePass() calls it after
a successful write.

updatePass() is the one method both doors go through — User\SaveEditPassController (an
administrator, or the user changing their own) and UserPassReset\SaveResetController (completing a
reset). 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 by
getUserIdForHash(), and marking it used costs nothing and leaves less to reason about.

Deliberately not included

User::updateOnLogin() (LDAP sync) and UserPass::migrateUserPassById() also write the pass
column 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 expected from
var/cache/CompiledContainerWeb40021031301.php. That is the documented stale-compiled-container
trap, not a wiring fault — after clearing var/cache, the container hands UserService a real
UserPassRecover.

…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.
@blaipr
blaipr merged commit 04e390e into main Sep 7, 2026
8 checks passed
@blaipr
blaipr deleted the fix/changing-a-password-spends-the-reset-tokens branch September 7, 2026 23:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant