Skip to content

Password managers can capture the TOTP setup QR and lock users out of login #945

Description

@dknauss

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

  1. Capture. The user opens their profile to set up TOTP. The QR renders (otpauth://totp/..., class-two-factor-totp.php generate_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.
  2. Incomplete setup. The user does not click the inline "Verify" button (class-two-factor-totp.php:408) that actually performs POST /two-factor/1.0/totp and stores _two_factor_totp_key. They may click "Update Profile" and leave, assuming they are done. (Since fix: Ensure two-factor methods are fully configured before enabling them #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.)
  3. 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.
  4. Rate limit. Repeated autofilled-and-wrong submissions trip Two_Factor_Core rate limiting (see Rate-limit error message is alarming and its countdown is stale for legitimate users #918, Email resend bypasses retry delay in 2FA challenge flow #847).
  5. Fallback also blocked. The UI offers an emailed code as a fallback, but requesting/using it is subject to the same lockout (Email resend bypasses retry delay in 2FA challenge flow #847, Email provider "Resend Code" button remains visible and interactive while user is rate-limited #920), so the legitimate emailed code also fails.
  6. Copy obscures it. The failure and rate-limit messaging historically didn't explain any of this (Rate-limit error message is alarming and its countdown is stale for legitimate users #918/Failed-attempt warning addresses two mutually exclusive audiences simultaneously #919, addressed in Improve rate-limit UX: calm error message and hide resend button during lockout #921/fix: reword mixed-audience login failure notice to be informational #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)

  1. Install 1Password or enable Apple Passwords in the browser.
  2. Go to your WordPress profile with Two-Factor active and expand the Authenticator App (TOTP) setup.
  3. 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).
  4. Accept the save, but do not click "Verify" and do not complete setup.
  5. 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 the otpauth:// 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 TOTP codes from Apple Passwords are wiped on the login screen #880/fix: prevent autofilled TOTP codes from being wiped on login #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 Email resend bypasses retry delay in 2FA challenge flow #847/Move rate-limit check before provider pre-processing, preserve email token #917) so a user poisoned by autofill still has a working escape hatch.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions