Skip to content

🛡️ fix: Pin Agent Runs to the Validated Model - #16800

Open
basnijholt wants to merge 2 commits into
LibreChat-AI:devfrom
basnijholt:fix/agent-model-parameter-hardening
Open

basnijholt wants to merge 2 commits into
LibreChat-AI:devfrom
basnijholt:fix/agent-model-parameter-hardening

Conversation

@basnijholt

@basnijholt basnijholt commented Oct 5, 2026 •

Copy link
Copy Markdown

Summary

This fixes a model-authorization bypass in agent runs, with a potential credential and conversation-data disclosure path through user-supplied fallback clients. LibreChat validates agent.model against the user's allowed models, but free-form model_parameters can change what the downstream client actually calls after that check. Permission to configure an agent must not grant permission to choose an unchecked model, provider, or destination for requests made by the server.

For example, an agent can declare an allowed model such as gpt-4o-mini while setting model_parameters.modelKwargs.model to a restricted model such as gpt-4o. Validation accepts the declared model, but the OpenAI-compatible client spreads modelKwargs into the request after its normal model field, replacing the validated value. This PR removes that override before provider configuration so the request uses the validated model.

Security impact and prerequisites

The relevant attacker is a user who can supply an agent's model parameters and cause that agent to run. The agent create/update schema accepts these parameters as a free-form record, so controls in the model picker alone do not enforce the boundary.

Path What can happen before this fix Conditions and impact
modelKwargs.model The provider request selects a different model from the one LibreChat authorized. An affected OpenAI-compatible client and provider credentials that can access the substituted model. Bypasses LibreChat's model restrictions and can incur charges for models the user is not allowed to use.
modelKwargs.models, route, or fallbacks An upstream router can choose another model without LibreChat validating that choice. Requires a gateway that honors those routing fields, such as the corresponding OpenRouter or LiteLLM options. The gateway's own permissions still apply, but LibreChat's allowed-model check no longer constrains the actual selection.
Top-level fallbacks After a qualifying primary-call failure, the agents runtime constructs another client using the supplied provider and client options, including its model and endpoint, and retries the conversation. If that destination is reachable, conversation content sent in the retry can leave the configured provider boundary. If the fallback SDK obtains a server-side environment credential, such as OPENAI_API_KEY, it can send that credential to the user-chosen endpoint. User-selected destinations also create a server-side request forgery risk, subject to network egress controls.
Google apiVersion A model option changes the API version used to construct the Google request URL. This is transport configuration crossing in through user model parameters. Blocking it is additional transport hardening; this change alone does not demonstrate arbitrary-host access or credential theft through apiVersion.

Credential disclosure is conditional on fallback activation, SDK credential resolution, and endpoint reachability. The PR's regression tests verify option filtering; they do not demonstrate a live credential-exfiltration attack.

How the fix closes these paths

Previously, initializeAgent only overwrote the top-level model. That left nested request overrides and alternate fallback clients intact. It now calls pinAgentModel before handing parameters to the provider configuration builder:

initializeAgent
  pinAgentModel(modelOptions, agent.model)
    remove top-level fallbacks and modelName
    remove modelKwargs.{model, models, route, fallbacks}
    set model to agent.model
  getOptions({ model_parameters: finalModelOptions })

Removing the nested routing fields and the top-level modelName alias prevents these parameters from replacing or routing around the validated model. Removing the top-level fallback list prevents these parameters from creating an unchecked alternate client when the primary request fails. The Google builder separately strips apiVersion alongside its existing blocked transport options. Unrelated options, such as temperature and service_tier, remain available.

The filtering runs when an agent is initialized, so it also applies to existing saved agents without a data migration. This is a targeted fix for the listed override paths, not a claim that all provider-specific routing options or outbound-request paths have been audited.

Type of change

  • Bug fix

Testing

Added regression coverage in:

  • packages/api/src/agents/__tests__/initialize.test.ts: removes top-level fallbacks and nested routing keys, prevents modelKwargs.model from replacing the validated model, and preserves unrelated options.
  • packages/api/src/endpoints/google/llm.spec.ts: ignores a model-option apiVersion override while preserving the selected model.

Follow-up head 01aed014ec3658ad59faa4de42a69c3984ee056a: 12 focused pin/provider/initial-and-child regressions passed; 187 unrelated tests were filtered out. The API build and scoped lint/import/format checks passed. npx tsc --noEmit was run but remains blocked by four missing Lodash declaration errors in unchanged files; changed fixture errors are cleared. No live provider requests were run. Exact-head CI and independent review are pending.

Screenshots / recordings

No user-facing UI change.

Compatibility risk

Saved agents that rely on the removed fallback or routing fields will lose that behavior: they will use their validated model, and a primary-call failure will no longer activate a fallback supplied through those fields. Google apiVersion overrides supplied through model options are also ignored. No schema migration or dependency change is included.

Agent model_parameters are free-form. A saved top-level fallbacks list,
model routing keys in modelKwargs, or a Google apiVersion could change
which model, endpoint or credentials an agent run uses, outside what
validateAgentModel and the server configuration allow.

initializeAgent now pins provider options to agent.model: it drops a
top-level fallbacks list and the modelKwargs keys that select another
model (model, models, route, fallbacks); other options pass through.
The Google config builder also drops apiVersion alongside the other
transport-level keys it already blocks.
Copilot AI balanced review requested due to automatic review settings October 5, 2026 22:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The modelName alias can still retain an unvalidated model selection.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Pins agent runs to validated models and blocks unsafe provider overrides.

Changes:

  • Removes fallback and nested model-routing overrides.
  • Blocks Google apiVersion overrides.
  • Adds regression coverage.
File Description
packages/​api/​src/​agents/​pin.ts Adds model-pinning sanitization.
packages/​api/​src/​agents/​initialize.ts Applies sanitization during initialization.
packages/​api/​src/​agents/​__tests__/​initialize.test.ts Tests routing-key removal.
packages/​api/​src/​endpoints/​google/​llm.ts Blocks client-provided API versions.
packages/​api/​src/​endpoints/​google/​llm.spec.ts Tests API-version filtering.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/api/src/agents/pin.ts Outdated
Comment on lines +8 to +21
type PinnedModelOptions<T, M> = Omit<T, 'fallbacks'> & { model: M };

/**
* Pins an agent's provider options to its validated model. `model_parameters` is free-form, so
* a saved agent could otherwise pick models that `validateAgentModel` never checked against the
* user's allowed models: a top-level `fallbacks` list makes the agents runtime build a second
* client from user-chosen provider, model and baseURL (with the server's credentials) when the
* primary call fails, and `modelKwargs` routing keys redirect the provider request itself.
*/
export function pinAgentModel<T extends Record<string, unknown>, M>(
modelOptions: T,
model: M,
): PinnedModelOptions<T, M> {
const { fallbacks: _fallbacks, ...options } = modelOptions;
@basnijholt

basnijholt commented Oct 5, 2026 •

Copy link
Copy Markdown
Author

@berry-13 @danny-avila, since I'm having a hard time getting my PRs merged here, I will not solve any of the issues suggested by the AI reviewer. I would just suggest to take a look here because it solves a real security issue. Feel free to push to my branch.

@codegraph-librechat codegraph-librechat Bot added the 🗺️ Backend Infra codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) label Oct 12, 2026
@lia-by-librechat

lia-by-librechat Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Head 01aed014ec3658ad59faa4de42a69c3984ee056a: also reject top-level modelName, with initial/child and real provider-builder regressions. Focused tests: 12 passed. API build and scoped lint/import checks passed. Local tsc --noEmit is blocked by four missing Lodash declaration errors in unchanged files; fixture errors are cleared. CI and independent review are pending for this new head; earlier reviews do not cover it.

@lia-by-librechat

lia-by-librechat Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Local inference-boundary candidate: aa44c2248bb35dd7c1bf71e040dbfab49d2a295f. It separates typed user settings from SDK configuration, retains canonical input for same-agent reinitialization, and honors final parameter/envelope drops. Local checks passed: 1,206 tests, API/data-schemas typechecks, API build, and PR-scoped static checks. Independent local audit completed with no findings at this exact local head. This does not review the unchanged remote PR head. Lighthouse is blocked by missing Chrome.

Not published: the App push was rejected because merging upstream dev includes workflow changes and the App lacks workflows permission. The PR remains at 01aed014ec3658ad59faa4de42a69c3984ee056a. A maintainer needs to sync dev into this existing fork branch before the code-only follow-up can be applied. No replacement PR or force push.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🗺️ Backend Infra codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants