Surfaced by QA run #10663 (identity-auth.two-factor-verify-to-activate / -enrollment-reveal) at 79ebb37. Existence + mechanism only; full reproduction withheld pending maintainer review per the auth/authz disclosure carve-out — available in the QA session on request.
What
Calling POST /api/v1/auth/two-factor/enable while 2FA is already active rotates the stored TOTP secret in place on the same sys_two_factor row and leaves verified = 1 carried over from the prior enrollment. The replacement secret — which no code has ever confirmed — becomes immediately live at the sign-in challenge, while the previously verified secret goes dead. Not a privilege escalation (the call requires a live session and the account password, and such an actor could call /two-factor/disable anyway).
Impact
- Integrity of the flag:
verified no longer describes the secret actually stored — any code or audit trusting "this factor was user-confirmed" is wrong after a re-enroll. - Availability: a user who re-enrolls and mis-scans the QR is locked out instantly — the old authenticator is already dead and there is no confirmation step to catch it.
Mechanism (fix-oriented)
The enable path treats an already-enrolled account the same as a fresh one, overwriting the secret without resetting verified to false or requiring a re-verification before the new secret is honored at the challenge. Fix direction: on re-enroll, set verified = false and gate the new secret on a fresh verify (or reject enable when already enabled and route through a dedicated rotate flow).
Related
Same family: sys_user.generate_backup_codes silently re-provisions on an already-enabled account too (see the non-sensitive lockout card #10681).
QA-source: #10663 · identity-auth.two-factor-verify-to-activate · clause 2 / negative 1
Surfaced by QA run #10663 (
identity-auth.two-factor-verify-to-activate/-enrollment-reveal) at79ebb37. Existence + mechanism only; full reproduction withheld pending maintainer review per the auth/authz disclosure carve-out — available in the QA session on request.What
Calling
POST /api/v1/auth/two-factor/enablewhile 2FA is already active rotates the stored TOTP secret in place on the samesys_two_factorrow and leavesverified = 1carried over from the prior enrollment. The replacement secret — which no code has ever confirmed — becomes immediately live at the sign-in challenge, while the previously verified secret goes dead. Not a privilege escalation (the call requires a live session and the account password, and such an actor could call/two-factor/disableanyway).Impact
verifiedno longer describes the secret actually stored — any code or audit trusting "this factor was user-confirmed" is wrong after a re-enroll.Mechanism (fix-oriented)
The enable path treats an already-enrolled account the same as a fresh one, overwriting the secret without resetting
verifiedto false or requiring a re-verification before the new secret is honored at the challenge. Fix direction: on re-enroll, setverified = falseand gate the new secret on a fresh verify (or reject enable when already enabled and route through a dedicated rotate flow).Related
Same family:
sys_user.generate_backup_codessilently re-provisions on an already-enabled account too (see the non-sensitive lockout card #10681).QA-source: #10663 · identity-auth.two-factor-verify-to-activate · clause 2 / negative 1