Repository navigation
Enhance sign-in password handling and memory login validation - #3337
Merged
Merged
Conversation
Two gaps in the credential-not-provided guard from #593, both confirmed by a probe against the merged code: - A login the user saved in WebBrain's local memory ("QA portal login: qa-tester / value"; memory refuses text that says "password") was refused at the sign-in field, while the same value in the WebBrain profile was typed. Memory was not a source. - Any lone password field whose form did not create an account counted as a sign-in, so "Fill out this form with random info" was refused on a form's lone "Password" field with "Submit", and "generate a strong Wi-Fi password" on a "Wi-Fi password" field with "Apply". Memory: - Active local memory notes are a source when memory is enabled, and, like the profile, not in a Cloud bot-memory session. - Only notes the user wrote count (user-memory.js userAuthoredMemoryRecords, source "manual": /memory --add, an edit in Settings, or an import or Profile Sync that carries that source). Every auto-capture path (chat turns, clarification answers, completed forms) saves source "auto" with text an extraction model wrote from the turn, whose assistant reply and clarify questions can quote the page, so those notes are excluded. - So that source keeps naming who wrote the text: an auto-capture rewrite of a manual note now makes it "auto"; a Settings edit that changes the text makes it "manual" (update_user_memory passes it, and a save that only changes the kind leaves the source); re-saving the same words up to case keeps the newer author's text with its source, since a password in it is case-sensitive. - The refusal now names "a WebBrain memory note they saved". Field rule (credential-fields.js, the same file in both builds): - A lone password field is a sign-in's only with sign-in evidence: a username, email or login field in its form (outside a form, among the root's form-less fields), a submit control that signs in or goes to the next step ("Sign in", "Log in", "Next", "Continue", "Enter", and those words in twelve other languages), or a name or label that says sign-in, login or account. autocomplete current-password and a field naming the current password stay guarded, and every existing exemption still comes first. - content.js _fieldMeta (both builds) and the Chrome CDP selector probe report the new usernameFields fact with the same code. Field facts without it (a route that did not read the form) keep the old rule. - newPasswordValue now takes only fields marked as new (autocomplete new-password, a new or repeated password label, a confirmation beside it, an account-creating submit), so a value made up for a lone password field never becomes a source for a later sign-in. Docs: security-model.md, guards.md, architecture.md (memory source), and the guard registry summary. Tests: test/credential-not-provided.mjs and test/credential-not-provided-dom.mjs; the new cases fail on the old code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the user's Review of the follow-up found real sign-ins the first cut of the lone password field rule left unguarded, all guarded on main, and two ways a memory note's provenance could be wrong. Field facts: - A password input in a web component's shadow root has no form owner, and usernameFields counted only that shadow root, so a light-DOM form of Email/Username and Password components with "Sign in" read as a lone field. A field's form is now its form owner or the <form> around the component across shadow boundaries; outside a form, the form-less part of its document across open shadow roots. - Outside a form no button was read, and the username test missed aria-labelledby labels, phone fields and customer or member numbers. Every visible button of the field's form (button, input submit/button/image, role=button, a button component whose label is slotted in) is now read into buttonTexts, nearest first, and any of them saying sign in, log in, next, continue, enter, verify, unlock or authenticate (more languages now) is evidence. A form with no button of its own (a dialog footer submits it) reads the document's form-less buttons. - An icon-only submit lost its aria-label to whitespace text, and a "Show" toggle before "Log in" hid the sign-in. Text is trimmed before falling back to aria-label, aria-labelledby or title, and any button counts. formSubmitText stays the first submit control, for "Create account". - A password step's hidden username or identifier now counts (not generic ids such as user_id or flow), so #hu-pass (hidden username + "Submit") is guarded, as the second step of a sign-in. - The facts are computed by one function, passwordFieldFacts in the shared rich-text-toolbar-heuristic.js, which content.js (both builds) and the Chrome CDP selector probe already load, instead of two copies of the code. Form-less passwordFields now counts only form-less fields (it counted every password field of the page, 7 on the DOM fixture, so a form-less sign-in beside any other password field read as new). Memory: - The dedupe let auto-capture repeating a note the user saved (on by default) take it over: the note became "auto" with the model's casing, and stopped counting. Now auto-capture repeating a manual note leaves its text, source and confidence; the user's own save, or a capture over a captured note, takes the new text and source. - Before the follow-up, an auto-capture rewrite of a manual note kept "manual" with the model's text and confidence. A note counts now only with confidence 1, which /memory --add saves and a Settings edit that changes the text sets; an auto-capture that keeps the user's words does not lower it. A legacy rewrite whose model reported exactly 1.0 cannot be told apart, and is documented. Docs: security-model.md, guards.md, architecture.md and the guard summary. Tests: four unit tests (dedupe, legacy confidence, any-button evidence, mirror check of the heuristic and user-memory files) and five DOM fixture pages (components, buttons, a form-less tel login, a dialog footer) through both builds' content.js and the CDP probe; they fail on the previous commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
credential-not-provided refused outright when text the model chose for a
sign-in password field was not in the places it reads (messages, clarify
answers, attached files, profile, the user's memory notes, MemCode, a
password WebBrain set). Those are not all the places a user can give a
password: a test form's documented login, or one kept where WebBrain does
not look, left the run stuck with no way through. The owner chose a card:
a wrong refusal should cost one click, while a guessed password must still
never be typed unasked.
The tool batch now pauses such a call before anything is typed and asks on
WebBrain's hard unverified-step card, after the call's action pipeline
returns, so the card waits outside the action deadline. It names the site
and the field ("Type this into a sign-in password field on <host>?"), says
the text is not a password the user gave in this conversation, their
profile or their saved notes, and shows the text masked (first character
and a dot per other character, only dots under four), the way the submit
card shows a password field as [redacted]; with strict secret handling it
shows none of it. Buttons: "Type it" / "Don't type it".
"Type it" runs the call again through the same typing-target preflight with
the approval, and it types only while the re-resolved target is still the
sign-in password field and site the card named (credential_field_changed
otherwise). That text then counts as given on that host for the rest of the
run. "Don't type it", a timeout answer, an unattended scheduled run or a
caller without a card returns the existing refusal (credential_not_provided,
noDispatch, retryable false), and the same text there is refused again
without a card. Cloud and MCP relay the card as they do the other hard
cards; it never times out. Lean mode and Strict checks keep it, saved
workflow replays are still unchecked, and page text is still no source.
The registry entry asks on both outcomes, and test/guard-policy.mjs lists it
as the one hard guard that asks when it knows. Both builds; all 23 locales
translate the new card strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sign-in password card docs compared its masking to the submit confirmation card and quoted that card's placeholder as "[redacted]". The submit probe (fieldValue in agent.js) writes "[password redacted]", so a reader checking the claim against the code would not find it. The guards doc now also says that a Cloud run's update log, which replaces typed text with "[redacted typed text]", still holds the masked preview of a relayed card, so nobody assumes Cloud keeps none of the text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of the sign-in password card found two problems. The card, and the answers kept for it, named the top-level tab's host even for a field in a frame from another origin. On a shop page with a bank's sign-in iframe, a guess approved for the shop's own sign-in field was then typed into the bank's field with no card. _credentialTargetHost now takes the site from the field's frame (webNavigation.getAllFrames; a frame at about:blank or about:srcdoc takes the site of the frame above it), or, when the preflight named no frame, from the document that answered the guard's own field probe. That site goes on the card, into the decision key and into the approval match. A field whose site cannot be read (a data: frame, a frame that is gone) is refused without a card instead of being named by the page's host. The rule that a lone password field is a sign-in's only with sign-in evidence typed guesses into real sign-ins unasked: a re-authentication with only "Password" and "Confirm", a "NetID" and "Password" form with "Submit", a PIN step whose account is a generic hidden id with "OK". main guarded all three. Now that the guard asks instead of refusing, a wrong "sign-in" costs the user one click, while a wrong "no sign-in" types a guess silently, so a lone password field with no evidence either way counts as a sign-in's and asks. Only a field that names a network's key (Wi-Fi, WLAN, wireless, WPA, WEP, SSID, hotspot, network key) is left out, so a generated Wi-Fi password is still typed unasked. "Confirm" and its translations join the re-authentication button words, which give the reason the model reads. "Fill out this form with random info" on a lone "Password" with "Submit" now asks once before typing. Outside a form, passwordFields counts every form-less password field, so a form-less sign-in shown beside a form-less sign-up read as a new password and was typed unasked (also on main). The password facts now report formless, and there a button that signs in keeps such a field a lone one; the sign-up's confirmation stays a new password. Tests: four new tests per build in test/credential-not-provided.mjs (the form-less sign-in, random info through the batch, the field's frame's site, the probe's document), the field-rule tests rewritten, and DOM fixtures for the re-authentication, NetID, PIN and side-by-side pages through both builds' content.js and the Chrome CDP probe. They fail on the previous commit. Docs: security-model.md, guards.md and the registry summary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of c0496edc5 found four ways a guessed sign-in password was still typed with no card, or named by the wrong site. The network-key exemption matched "Network password", which is how many companies name the directory (AD or SSO) account's password, and "ssid" inside "sessid" or "passid". A lock page's lone "Network password" with "OK" was typed unasked; main guarded it. Only wireless words leave a field out now (Wi-Fi, WLAN, wireless, WPA, WEP, SSID, hotspot, network key, network security key), and SSID, WEP and WPA must not continue a longer word, though a separator may join them (ssid_pass, router_wpa_key). The top frame's site was the tab's address as it stands: every about:blank popup was "about", data: pages "data", blob: pages "blob:https:" and an unreadable tab "", so a "Type it" given on one such page covered another site's sign-in shown the same way. The top frame now takes its site by the frame rule: about:blank, about:srcdoc, data: and an unreadable address have none and are refused without a card, and a blob: page has its creator's site. Without a frame from the preflight, the guard's probe of the field was sent to every frame, and so was the preflight's own probe. Once an iframe tool had injected the content script into a subframe, that frame's quick "stale or unavailable" answered first, so the field read as unknown and the guess was typed unasked by the write, which also goes to every frame. The guard now asks each frame from webNavigation on its own and takes the site from the frame that holds the field (the document that answered must be of that frame's site); a selector naming a sign-in field in two frames is refused without a card. The preflight's probe of a ref, selector or index asks only the top frame, whose answer it is bound to. A second visible password field made any form a sign-up's, before the account field or sign-in button was read (also on main). A bank's username, "Password" and "PIN" with "Log in", or a sign-in with an off-screen autofill decoy, typed the guess unasked and then counted it as a password WebBrain set, for every later sign-in in the tab, since those were kept per tab with no site. Such a field now reads as a new password only when the form creates an account or shows no sign-in (no account field, no "Log in" or "Sign in" button); otherwise the lone-field rules decide, and a sign-up whose "Email" nothing else marks, with "Submit", asks for its "Password" while "Confirm password" stays new. A password WebBrain set counts only on the site of its field, read before the write goes out; the result records it as newPasswordSite for a restored conversation, and a result without one counts nowhere. Tests: seven new or rewritten tests per build in test/credential-not-provided.mjs (the network words, the second password field, the top page's site, every frame asked, the preflight's top frame, new passwords by site, and the form-less "Next" case), and DOM fixtures for the bank, decoy, sign-up and "Network password" pages through both builds' content.js and the Chrome CDP probe. On c0496edc5's sources 14 of 73 unit tests and 3 of 6 DOM tests fail. Docs: security-model.md, guards.md and the registry summary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-ins Review of 892338c8d found two ways a guessed sign-in password was still typed with no card. The site a password WebBrain set counts on was read from the preflight's frame, and since 892338c8d the preflight probes only the top frame. A write to a ref in a subframe arrived with no frame, so its password was kept for the top page's site: a guess typed into a cross-site sign-up frame's new-password field was then typed unasked into the page's own sign-in. The site now comes from the frame that holds the field: the preflight's when it read the field, or else the one the guard's own per-frame probe found for this call (kept per call, so the page is asked once). The site read sends no probe of its own, so a write whose field no frame answered for, or one the guard did not probe (text the user gave, a replay, WebBrain's own writes), keeps its password for no site. autocomplete="new-password" made a field a new password before its form was read (also on main). Many sign-in forms set it on their password input to stop the browser's autofill, so a guess there was typed unasked and then counted as a password WebBrain set for the site's next sign-in. A form's only password field marked new-password is now a sign-in's when its submit control says "Log in" or "Sign in" (or a translation), or another of its buttons does while no control creates an account. A field whose own name or label says new, and one beside another password field, stay new. Tests: two new tests per build in test/credential-not-provided.mjs (a cross-site sign-up frame, then the page's and the frame's sign-ins, through the tool batch; the new-password rule and the review's sequence through executeTool), and a DOM fixture for an anti-autofill login and a new-password sign-up through both builds' content.js and the Chrome CDP probe. On 892338c8d's sources 4 of 77 unit tests and 3 of 6 DOM tests fail. Docs: security-model.md, guards.md and the registry summary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…another Review of 1b0a9f1b0 found that the guard checked the field the typing-target preflight resolved, and the dispatch could then type into another one (also on main). By default (typing-target-identity advises), Chrome's focused type_text whose bound field lost focus types into the field focused now, and a type_text selector the preflight did not pin types into the selector's current match. Firefox binds such a selector's current match, or sends the write unbound to every frame. A two-step sign-in that focuses its password field after the username step's Next took the guess there with no card. Where the dispatch types into a field other than the one checked, it now runs the guard against that field first (_retargetedCredentialBlock): the element it pinned or bound, as its probe read it, or else the guard's own probe of the field focused now or of the selector's match (in the top frame for Chrome, where it types; in each frame for Firefox's unbound write). Strict checks still refuse first, and the typing-target-identity note is left only when the text goes through. An approved password does not follow focus to another field (credential_field_changed). Such a refusal comes from inside the dispatch. It is returned without the call's result finalization, as the guard's own is: after a "Type it" the batch runs the call again with the same dispatch state, and the finalized first refusal would have stayed the result of the rerun that typed. Tests: in test/credential-not-provided.mjs, three Chrome tests (focus moved to a sign-in password field, through executeTool and through the tool batch with the real dispatch, "Don't type it" and "Type it"; a selector without a pin) and one Firefox test (a selector without a binding, matching in a frame from another site). On 1b0a9f1b0's sources 4 of 81 fail. Docs: security-model.md and guards.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of 8e6e6e5cc found that Firefox typed a guess into a sign-in
password field with no card (on main too). The content `type` handler
types a type_text with no selector into the field the last click focused
when focus has since gone to the body or to that field's shadow host
(typeText's recent-target fallback, kept for 30 s). The guard's probe of
the focused field (field_value_digest {focused: true}) read only the
active element, so it found no field in any frame, and a target that
cannot be read is typed as before. A virtual-keypad sign-in that blurs
its password field on focus took the guess after click({selector}).
The guard's probe (_credentialTargetFieldMeta, both builds) now asks for
the field a selectorless `type` writes to (`typeTarget: true`). Firefox's
field_value_digest answers it with the same fallbacks as typeText: the
recent editable target when focus is on the body, or on that target's
shadow host. Other callers of the focused digest are unchanged, and
Chrome's content script, whose typing has no fallback, ignores the flag.
Tests: in test/credential-not-provided-dom.mjs, a Firefox test runs the
real content.js and agent.js on a keypad sign-in, for focus lost to the
body and to a component: type_text({text}) returns the card's question
and sends no `type`, and a selectorless `type` then writes to that
field. A Chrome test shows its `type` types nothing there. Both Firefox
cases fail on 8e6e6e5cc's sources. The focused-probe assertion in
test/credential-not-provided.mjs checks the flag. Docs: guards.md and
security-model.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also name the field's own component in the two docs that describe Firefox's selectorless type_text fallback, as the last review noted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ask before typing a sign-in password the user never gave, and count saved logins
|
@esokullu is attempting to deploy a commit to the esokullu's projects Team on Vercel. A member of the Team first needs to authorize it. |
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.
No description provided.