Skip to content

🔏 fix: Prevent Browser Autofill From Silently Dropping MCP CustomUserVars on Save#12770

Merged
danny-avila merged 2 commits into
danny-avila:devfrom
jschmetzer:fix/mcp-customuservars-prevent-autofill
Apr 22, 2026
Merged

🔏 fix: Prevent Browser Autofill From Silently Dropping MCP CustomUserVars on Save#12770
danny-avila merged 2 commits into
danny-avila:devfrom
jschmetzer:fix/mcp-customuservars-prevent-autofill

Conversation

@jschmetzer

Copy link
Copy Markdown
Contributor

Summary

The customUserVars form in CustomUserVarsSection.tsx renders 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:

  1. 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).

  2. 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's Controller therefore 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 into pluginauths, 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.tsx for the analogous actions-credential input:

type="new-password"
autoComplete="new-password"

plus the two vendor-specific ignore attributes:

data-lpignore="true"     // LastPass
data-1p-ignore="true"    // 1Password

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-form still 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's Input forwards {...props} through to the native <input>, the new attributes flow through without a component change.

Change Type

  • Bug fix (non-breaking change which fixes an issue)

Testing

Manual verification in a local deployment:

  1. Without this fix, with any password manager active (Chrome built-in is enough), configure a remote MCP server that has customUserVars defined. Fill in all fields via the Settings UI. Save.
  2. Reopen the dialog. All fields show "Unset" despite being filled and saved.
  3. Query db.pluginauths directly: every field has an identical short hex value (the encrypted form of an empty string). Any MCP tool call that needs one of those credentials fails server-side with an auth error.
  4. Apply this patch.
  5. Repeat the configuration. After save, the fields show "Set" and db.pluginauths has distinct encrypted values of meaningful length. Tool calls succeed.

Test Configuration

  • macOS 14, Chrome 138 with built-in password manager enabled
  • LibreChat main at commit 7cf8b84
  • Remote MCP server with 16 customUserVars defined in librechat.yaml (mcpServers.<name>.customUserVars)

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code (a block comment above the new attributes explains the React-autofill interaction)
  • I have made pertinent documentation changes
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes
  • Any changes dependent on mine have been merged and published in downstream modules.
  • A pull request for updating the documentation has been submitted.

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.

…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.
@danny-avila danny-avila changed the base branch from main to dev April 22, 2026 14:59
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.
@danny-avila danny-avila changed the title fix: prevent browser autofill from silently dropping MCP customUserVars on save 🔏 fix: Prevent Browser Autofill From Silently Dropping MCP CustomUserVars on Save Apr 22, 2026
@danny-avila danny-avila merged commit 40742f9 into danny-avila:dev Apr 22, 2026
8 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants