fix(extension): do not fill a 2FA code into a form with empty credentials - #331
Merged
Conversation
…ials The standalone 2FA pass added in the previous release fired independently of the credential fill, so on a single-step login page the code could land while the username and password were still empty. It looks broken, and it burns the user's attention on a code they cannot submit yet. Two things caused it. The pass ran immediately from refreshMatches while the credential autofill waits 500ms and then polls for its form, so the code arrived first even when both were going to fire. Worse, the two paths disagreed on when a page is unambiguous: the credential fill requires exactly one matching login, while the code fill only required exactly one match CARRYING A SECRET. On a host with two saved logins where one has a TOTP, the credential fill correctly stood down as ambiguous and the code fill went ahead anyway, so the credentials were never filled at all. The standalone pass now stands down whenever a visible login form still has an empty password box. That fill owns the code and writes username, password and code together, which removes the ordering race entirely rather than papering over it with a delay. It is gated on an empty password rather than on the mere presence of a form so the SPA case still works: a site that fills credentials in one step and only then reveals its code box is filled, because by that point nothing is pending. The ambiguity rule now matches the credential path exactly. Net behaviour: picking a login from the picker, or autofill-on-load on a single-step form, fills all three together; a 2FA-only second page still fills on its own; a form with empty credentials is left alone; and with autofill off the code is still only ever filled by clicking the emblem. The decision moves out of the content script into a pure predicate in utils, alongside the existing one-time-code detector and field classifier, so the policy is unit-testable rather than trapped inside defineContentScript. Nine cases cover the table above, including the regression itself.
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.
Follow-up to #328. Fixes a regression that shipped in 1.26.0.
The bug
On a single-step login page the 2FA code could be filled while the username and password were still empty. It looks broken, and it puts a code on screen that cannot be submitted yet.
Two causes:
Ordering.
maybeAutofillOtpran immediately fromrefreshMatches, while the credential autofill waits 500ms and then polls for its form. Even when both were going to fire, the code arrived first.Disagreeing conditions. The credential fill requires
matches.length === 1; the code fill only requiredtotpMatches().length === 1. On a host with two saved logins where only one carries a TOTP, the credential fill correctly stood down as ambiguous and the code fill went ahead regardless, so the credentials were never filled at all. This is the more likely path to what was reported.The fix
The standalone pass now stands down whenever a visible login form still has an empty password box. That fill owns the code and writes username, password and code in one pass, which removes the ordering race outright rather than papering over it with a longer delay.
Gating on an empty password rather than on the mere presence of a form keeps a real case working: an SPA that fills credentials in one step and only then reveals its code box still gets filled, because by that point nothing is pending.
The ambiguity rule now matches the credential path exactly: exactly one matching login, and that login carries the secret.
Resulting behaviour
Tests
The decision moved out of
content.tsinto a pure predicate inextension/utils/otp-autofill.ts, alongside the existing one-time-code detector and field classifier. The policy was previously trapped insidedefineContentScriptand could not be tested at all.Nine cases cover the table above, including the regression itself (
hasPendingCredentialForm: truemust not fill), which returnstrueunder the old logic andfalsenow.Verification
npm run typecheckcleannpm test74/74 passing (65 before, 9 new)npm run buildsucceedsNot exercised against a live 2FA page in a browser.