WordPress / WordPress/two-factor
Password managers can capture the TOTP setup QR and lock users out of login
Nobody has claimed this yet.
- Dominant language
- PHP
- Stars
- 825
- Forks
- 187
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 17
Description
Problem:
Password managers can capture the TOTP secret from the setup QR and then autofill a mismatched code at login, cascading into a rate-limited lockout that the email fallback can't rescue.
I've been gobsmacked by this myself with 1password, and I recently saw a colleague go through exactly the same confusion.
Summary
Modern browser password managers that support built-in TOTP — 1Password, Apple Passwords / iCloud Keychain, Google Password Manager, Bitwarden, Dashlane, Proton Pass, NordPass, and others — can capture the otpauth:// provisioning URI encoded in the TOTP setup QR code and store it when the QR is visible on the profile page (depending on the manager and version, on page render or on an explicit scan/save action), before and independently of the user completing setup in Two-Factor. This creates a class of failures where the password manager holds a TOTP secret that the user's Two-Factor profile does not, and then "helpfully" autofills codes at login that cannot succeed. The resulting failed attempts push the account into the rate limiter, and because the same rate limiter also gates the emailed-code fallback, the user's escape hatch fails too.
This is not a single-vendor problem — any password manager with integrated OTP behaves this way, because it is the intended behavior of the otpauth:// standard. The existing tracker coverage is scattered and vendor-specific (Apple Passwords / Google in #880/#904, Safari prefill in #678, Bitwarden timing in #580), so there is no manager-agnostic description of the root cause or the cascade.
This issue is filed to capture the root cause and the end-to-end chain, which no single existing issue documents. Several issues cover individual links, but the origin (password-manager capture) and the cascade are undocumented.
The failure chain
- Capture. The user opens their profile to set up TOTP. The QR renders (
otpauth://totp/...,class-two-factor-totp.phpgenerate_qr_code_url()/js/totp-admin-qrcode.js). The password-manager browser extension can read the provisioning URI and offer to save a login item with an embedded OTP secret. The user accepts — reasonably believing this is how you set up 2FA. - Incomplete setup. The user does not click the inline "Verify" button (
class-two-factor-totp.php:408) that actually performsPOST /two-factor/1.0/totpand stores_two_factor_totp_key. They may click "Update Profile" and leave, assuming they are done. (Since #798, the profile save now refuses to enable an unconfigured method, so TOTP ends up not enabled — but the password manager still holds a secret.) - Login mismatch. At the next login, the user is challenged for whichever method is configured (often Email). The password manager, matching by domain rather than by method, autofills its stored TOTP one-time code into the challenge field. That code is wrong — either because TOTP was never enabled, or because it's being injected into an Email (
two-factor-email-code) field. The user submits it, unaware the manager pre-filled a bad value. - Rate limit. Repeated autofilled-and-wrong submissions trip
Two_Factor_Corerate limiting (see #918, #847). - Fallback also blocked. The UI offers an emailed code as a fallback, but requesting/using it is subject to the same lockout (#847, #920), so the legitimate emailed code also fails.
- Copy obscures it. The failure and rate-limit messaging historically didn't explain any of this (#918/#919, addressed in #921/#922), so the user has no way to understand why a code they didn't knowingly enter keeps failing.
The net effect: a user who never intended to misconfigure anything is locked out, and every affordance the UI offers to recover is either pre-poisoned by the password manager or gated by the same rate limiter.
Why this isn't already covered
| Link | Existing coverage | Gap |
|---|---|---|
| Two-button setup (Verify vs. Update Profile) saving an inconsistent state | #797 → fixed by #798 (#796, #157) | Data-integrity fixed, but password-manager capture is orthogonal and still happens |
| Autofilled code wiped/blanked at login (Apple, Google) | #880, PR #904; #678; #373; #420 | Those address the code being erased; this is about a wrong/mismatched code being injected |
| Autofill submitting too fast → 503 / needs several tries (Bitwarden) | #580, #429 | Reported per-vendor as a flaky "503"; never tied to autofill timing or the rate limiter |
| Rate-limit UX and stale countdown | #918 → #921 | — |
| Resend/email fallback bypassing or colliding with rate limit | #847, #920 → #917 | — |
| Failure-notice copy | #919 → #922 | — |
| Password manager captures the QR secret pre-setup | none | Unfiled |
| End-to-end cascade documented as one story | none | Unfiled |
Reproduction (capture step)
- Install 1Password or enable Apple Passwords in the browser.
- Go to your WordPress profile with Two-Factor active and expand the Authenticator App (TOTP) setup.
- Observe the password manager offer to save a login item with a one-time password from the visible QR (on page render or when you invoke its scan/save action, depending on the manager).
- Accept the save, but do not click "Verify" and do not complete setup.
- Log out and log back in. Observe the password manager autofill a TOTP code into the 2FA challenge field, even though TOTP was never enabled / the challenge is for a different method.
Notes / technical anchors
- The setup verify field already sets
autocomplete="off"(class-two-factor-totp.php:406), but that does not stop the extension from reading theotpauth://URI in the rendered QR — capture keys off the QR/provisioning URI, not the input field. - The login email field sets
autocomplete="one-time-code"(class-two-factor-email.php:367) and the login TOTP field sets it too (class-two-factor-totp.php:792), which is what invites cross-method autofill into whichever challenge is shown. - The login form itself carries form-level
autocomplete="off"(class-two-factor-core.php:1130), which interacts badly with the per-field hints — this is the same conflict already noted in #880/#904.
Possible directions (for discussion, not prescriptive)
- Reduce silent capture: consider whether the QR/provisioning URI can be revealed only on an explicit user action (e.g., behind a "Show QR code" control) so managers don't auto-detect it on page load. This trades a little convenience for far less accidental capture.
- Confirm intent before the manager can act: surface a clear "You must click Verify to finish — saving this in a password manager is not enough" affordance next to the QR.
- Detect and warn on mismatch at login: when a submitted code fails validation for the active method but would be a plausible TOTP, hint that an autofilled code from a password manager may be involved.
- Decouple the email fallback from the failed-TOTP rate limit (overlaps with #847/#917) so a user poisoned by autofill still has a working escape hatch.
Related
- Setup save integrity: #797, #796, #157 (fixed in #798)
- Autofill wiping / prefill at login (Apple, Google, Safari): #880, PR #904, #678, #373, #420
- Autofill timing → 503 / "works after several tries" (Bitwarden): #580, #429
- Rate limiting & fallback: #918, #847, #920 (PRs #921, #917)
- Failure-notice copy: #919 (PR #922)
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading class-two-factor-totp.php, js/totp-admin-qrcode.js, class-two-factor-email.php, and class-two-factor-core.php around the referenced lines. Review the related issues and fixes, especially #798, #880/#904, #918/#921, and #847/#917, before choosing a direction. Done means an agreed, tested mitigation addresses the QR capture or cross-method autofill without breaking TOTP setup or email fallback.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- php
- Domain
- authentication, security
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100