🔏 fix: Prevent Browser Autofill From Silently Dropping MCP CustomUserVars on Save#12770
Merged
danny-avila merged 2 commits intoApr 22, 2026
Conversation
…rs on save The customUserVars form in CustomUserVarsSection renders each credential as a plain `<input type="text">` with no autofill guards. This caused two user-visible problems: 1. Browser password managers (Chrome's built-in, 1Password, LastPass, Bitwarden) treat the fields as savable and offer to store values, which undermines the per-user credential model -- the whole point of customUserVars is that each user's own secrets stay in their own session and backend-encrypted, not cached by the browser. 2. When autofill fills a field, it does so via DOM mutation that does NOT fire React's synthetic `onChange`. react-hook-form's Controller therefore never sees the value, the form state stays `""`, and on submit the backend receives an empty string for every affected field. The user's typed credentials are silently dropped -- LibreChat stores 16 encrypted-empty-string rows in pluginauths, tool calls subsequently fail with "API key not set" errors, and the UI shows an "Unset" pill next to fields the user is certain they filled in. The fix matches the pattern already used in `SidePanel/Builder/ActionsAuth.tsx` for the analogous actions credential input: type="new-password" autoComplete="new-password" plus vendor-specific ignore attributes for LastPass and 1Password: data-lpignore="true" data-1p-ignore="true" (Modern browsers ignore `autocomplete="off"` for password-shaped fields, so the vendor attributes are the reliable defence against LastPass and 1Password, which have their own heuristics.) No behavioural change beyond preventing autofill: the input still accepts typed or pasted values, react-hook-form still collects them, submit still writes to the backend. The difference is that the values now actually reach the submit handler.
Condenses the rationale comment on the credential `<Input>` and adds a render test asserting that the autofill-prevention attributes (`type`, `autoComplete`, `data-lpignore`, `data-1p-ignore`) remain on the rendered input. The underlying bug (browser DOM mutations bypassing React's synthetic onChange) is severe enough that a future refactor accidentally dropping these attributes would silently re-introduce credential data loss.
jcbartle
pushed a commit
to jcbartle/LibreChat
that referenced
this pull request
May 11, 2026
…Vars on Save (danny-avila#12770) * fix: prevent browser autofill from silently dropping MCP customUserVars on save The customUserVars form in CustomUserVarsSection renders each credential as a plain `<input type="text">` with no autofill guards. This caused two user-visible problems: 1. Browser password managers (Chrome's built-in, 1Password, LastPass, Bitwarden) treat the fields as savable and offer to store values, which undermines the per-user credential model -- the whole point of customUserVars is that each user's own secrets stay in their own session and backend-encrypted, not cached by the browser. 2. When autofill fills a field, it does so via DOM mutation that does NOT fire React's synthetic `onChange`. react-hook-form's Controller therefore never sees the value, the form state stays `""`, and on submit the backend receives an empty string for every affected field. The user's typed credentials are silently dropped -- LibreChat stores 16 encrypted-empty-string rows in pluginauths, tool calls subsequently fail with "API key not set" errors, and the UI shows an "Unset" pill next to fields the user is certain they filled in. The fix matches the pattern already used in `SidePanel/Builder/ActionsAuth.tsx` for the analogous actions credential input: type="new-password" autoComplete="new-password" plus vendor-specific ignore attributes for LastPass and 1Password: data-lpignore="true" data-1p-ignore="true" (Modern browsers ignore `autocomplete="off"` for password-shaped fields, so the vendor attributes are the reliable defence against LastPass and 1Password, which have their own heuristics.) No behavioural change beyond preventing autofill: the input still accepts typed or pasted values, react-hook-form still collects them, submit still writes to the backend. The difference is that the values now actually reach the submit handler. * test: add regression guard for MCP customUserVars autofill prevention Condenses the rationale comment on the credential `<Input>` and adds a render test asserting that the autofill-prevention attributes (`type`, `autoComplete`, `data-lpignore`, `data-1p-ignore`) remain on the rendered input. The underlying bug (browser DOM mutations bypassing React's synthetic onChange) is severe enough that a future refactor accidentally dropping these attributes would silently re-introduce credential data loss. --------- Co-authored-by: Danny Avila <danny@librechat.ai>
vlasmo
pushed a commit
to fdj-united/Fdj-LibreChat
that referenced
this pull request
May 28, 2026
…Vars on Save (danny-avila#12770) * fix: prevent browser autofill from silently dropping MCP customUserVars on save The customUserVars form in CustomUserVarsSection renders each credential as a plain `<input type="text">` with no autofill guards. This caused two user-visible problems: 1. Browser password managers (Chrome's built-in, 1Password, LastPass, Bitwarden) treat the fields as savable and offer to store values, which undermines the per-user credential model -- the whole point of customUserVars is that each user's own secrets stay in their own session and backend-encrypted, not cached by the browser. 2. When autofill fills a field, it does so via DOM mutation that does NOT fire React's synthetic `onChange`. react-hook-form's Controller therefore never sees the value, the form state stays `""`, and on submit the backend receives an empty string for every affected field. The user's typed credentials are silently dropped -- LibreChat stores 16 encrypted-empty-string rows in pluginauths, tool calls subsequently fail with "API key not set" errors, and the UI shows an "Unset" pill next to fields the user is certain they filled in. The fix matches the pattern already used in `SidePanel/Builder/ActionsAuth.tsx` for the analogous actions credential input: type="new-password" autoComplete="new-password" plus vendor-specific ignore attributes for LastPass and 1Password: data-lpignore="true" data-1p-ignore="true" (Modern browsers ignore `autocomplete="off"` for password-shaped fields, so the vendor attributes are the reliable defence against LastPass and 1Password, which have their own heuristics.) No behavioural change beyond preventing autofill: the input still accepts typed or pasted values, react-hook-form still collects them, submit still writes to the backend. The difference is that the values now actually reach the submit handler. * test: add regression guard for MCP customUserVars autofill prevention Condenses the rationale comment on the credential `<Input>` and adds a render test asserting that the autofill-prevention attributes (`type`, `autoComplete`, `data-lpignore`, `data-1p-ignore`) remain on the rendered input. The underlying bug (browser DOM mutations bypassing React's synthetic onChange) is severe enough that a future refactor accidentally dropping these attributes would silently re-introduce credential data loss. --------- Co-authored-by: Danny Avila <danny@librechat.ai>
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.
Summary
The customUserVars form in
CustomUserVarsSection.tsxrenders each credential as a plain<input type="text">with no autofill guards. This causes two user-visible problems when configuring a remote MCP server's per-user credentials from the Settings UI:Password managers offer to save the values. Chrome's built-in manager, 1Password, LastPass, and Bitwarden all detect the fields as credential-shaped and prompt to store them. That cuts against the point of the per-user model (each user's secrets stay in their own browser session and backend-encrypted, not cached in a password vault).
More seriously: autofill silently empties the save. When a password manager fills a field via DOM mutation, it does not fire React's synthetic
onChange.react-hook-form'sControllertherefore never sees the value, form state stays"", and on submit the backend receives empty strings for every affected field. The user's typed credentials are silently dropped. Concretely this writes 16 encrypted-empty-string rows intopluginauths, every subsequent MCP tool call then fails with"API key not set"-style errors, and the UI shows an"Unset"pill next to fields the user is sure they filled in.The fix
Match the pattern already used in
client/src/components/SidePanel/Builder/ActionsAuth.tsxfor the analogous actions-credential input:plus the two vendor-specific ignore attributes:
Modern browsers ignore
autocomplete="off"for password-shaped fields, so the vendor attributes are the reliable defence for LastPass and 1Password (which have their own heuristics that ignore the standard attribute).No behavioural change beyond preventing autofill: the input still accepts typed and pasted values,
react-hook-formstill collects them, and submit still writes to the backend. The difference is that the collected values actually reach the submit handler now.Because
@librechat/client'sInputforwards{...props}through to the native<input>, the new attributes flow through without a component change.Change Type
Testing
Manual verification in a local deployment:
customUserVarsdefined. Fill in all fields via the Settings UI. Save.db.pluginauthsdirectly: every field has an identical short hexvalue(the encrypted form of an empty string). Any MCP tool call that needs one of those credentials fails server-side with an auth error.db.pluginauthshas distinct encrypted values of meaningful length. Tool calls succeed.Test Configuration
customUserVarsdefined inlibrechat.yaml(mcpServers.<name>.customUserVars)Checklist
Notes on the unchecked items: the change is four new HTML attributes on one
<Input>; I did not add a unit test because the behaviour is asserted at the DOM/browser-integration level (password-manager interaction is not exercised by the existing jest/vitest suites). Happy to add a snapshot test or similar if you'd like.