* 🔧 fix: Honor NO_PROXY for OpenID requests when PROXY is set
openidStrategy routed every OIDC request (issuer discovery, JWKS,
token endpoint, Microsoft Graph overage resolution) through
undici.ProxyAgent whenever PROXY was set. undici.ProxyAgent does not
consult NO_PROXY, so OIDC providers on internal networks that the
corporate proxy cannot reach failed at startup with ECONNREFUSED or
discovery timeouts, even when the issuer host was listed in NO_PROXY.
Replace ProxyAgent with undici.EnvHttpProxyAgent configured to use
PROXY for both protocols. EnvHttpProxyAgent applies the standard
NO_PROXY/no_proxy exclusion list per request host (suffix matching,
leading-dot domains, host:port entries, and *), so excluded hosts are
requested directly.
The agent is also memoized (keyed on PROXY + NO_PROXY) instead of
being constructed per request, so repeated OIDC calls reuse one
connection pool.
Fixes#13705
* fix: move OpenID proxy helper to api package
* chore: import order in openidStrategy.js
* chore: import order in openidStrategy.spec.js
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* ⚡️ refactor: Migrate @librechat/api build to tsdown
Replace Rollup with tsdown (rolldown + oxc isolated-declarations) for the
@librechat/api package build, mirroring the merged data-schemas migration.
- Add tsdown.config.mjs (cjs output, oxc dts, externalize all bare deps,
bundle first-party `~/` + relative imports)
- Annotate exports for isolatedDeclarations (codefix-driven). Collapse the
tokens.ts model->token maps to Record<string, Record<string, number>> and
switch validation.ts's runtime `files` field from z.any() to z.unknown()
so no explicit `any` is introduced
- Repoint package.json main/types/exports to tsdown's .cjs/.d.cts output
- Add src/telemetry.ts entry shim so the two index.ts entries don't collide
in oxc's flat dts output (stable dist/telemetry.{cjs,d.cts})
- Delete rollup.config.js
Build time ~36s -> ~0.5s. No runtime behavior change: 5712 unit tests pass,
both entries load via require(), legacy /api consumes them unchanged.
* 👷 ci: Hash packages/api/tsdown.config.mjs in build-api cache keys
The build-api cache keys hashed `packages/api/server-rollup.config.js`,
which never existed (api used `rollup.config.js`, now removed) — a copy-paste
artifact from the data-provider key that matched no file. Replace it with the
new `packages/api/tsdown.config.mjs` so edits to the build config (entry,
format, externals) bust the api build cache, matching the data-schemas key.
* Shared Role-Sync Core
* Environment Configuration
* Browser OpenID Wiring & improved shared component
* API Auth Wiring
* Improved Role Lookup
* added example for sync env
* small simplification
* protect existing manual assigned ADMIN Roles
* fix: Apply OpenID role-sync fallback for present-but-empty claims
Both role-sync call sites skipped on a falsy `openIdRoleValues`, treating an
empty claim string ('') the same as a missing claim and returning before
`selectOpenIdRole` could apply the configured fallback role. An IdP emitting
an empty roles claim for a user with no mapped groups left the stale local
role in place instead of the authoritative fallback.
Skip only when the helper returns `undefined` (missing/invalid), letting an
empty string flow through to fallback selection — consistent with how an
empty array is already handled. Adds regression coverage on both the OpenID
strategy and the remote-agent API auth paths.
* refactor: Address OpenID role-sync review feedback
- role.ts: reuse the shared escapeRegExp util instead of a local escapeRegex
duplicate, matching prompt/skill/user/userGroup methods (Copilot).
- openidStrategy.js / remoteAgentAuth.ts: make the tenantStorage.run callbacks
async so the documented ALS contract is satisfied and tenant context cannot
be lost during Mongoose execution; the wrapped lookups/updates are already
async, so behavior is unchanged (codex P2).
* fix: Harden OpenID role-sync claim and fallback handling
Addresses the second Codex review cycle (P2 findings):
- Apply fallback when the claim is absent: getOpenIdRolesForOpenIdSync now
returns an empty list (not undefined) when the token source exists but has
no usable claim value, so callers still run selection and assign the
configured fallback instead of leaving a stale elevated role. A truly
unavailable source still returns undefined and skips sync.
- Resolve group overage for access tokens too: the _claim_names/_claim_sources
overage path previously only ran for claimSource 'id'; Entra also moves an
oversized groups claim into access tokens, so 'access'+'groups' (the only
source supported by remote-agent API sync) now resolves overage as well.
- Allow system fallback roles for tenant users: getLibreChatRolesForOpenIdSync
treats SystemRoles (e.g. USER) as always-available canonical names, since
they are provisioned globally at startup and a tenant-scoped lookup may not
return them — preventing a spurious 'configured roles do not exist: USER'.
Adds unit and strategy-level coverage for all three.
* fix: Tighten OpenID role-sync tenant scoping and config validation
Addresses the third Codex review cycle:
- Constrain base-user role lookups to base roles (P2): findRolesByNames now
filters to roles with an unset tenantId when no tenant ALS context is active,
so a base user cannot match — and be assigned — a role that only exists within
a tenant. Tenant-scoped lookups remain controlled by the isolation plugin.
- Re-enforce tenant login policy after role sync (P2): when role sync changes a
tenant user's role, the OpenID strategy re-resolves the tenant appConfig and
re-checks allowedDomains, so a token cannot complete login under the previous
role's looser policy.
- Skip role-sync-specific validation when disabled (P3): getOpenIdRoleSyncOptions
returns disabled options before validating role-sync settings, so a stale or
mistyped value no longer breaks OpenID login while the feature is off.
Adds unit and strategy-level coverage for all three.
* fix: Run base role lookups under system context for strict isolation
Follow-up to the base-role scoping fix (Codex P1). With TENANT_ISOLATION_STRICT=true,
the tenant-isolation pre('find') hook throws on a context-less query before the manual
tenantId filter is honored, so base OpenID/remote-agent auth would 500 instead of
validating base roles. findRolesByNames now runs the no-context lookup inside
runAsSystem (SYSTEM_TENANT_ID), bypassing strict-mode injection while still applying an
explicit base-role (tenantId unset) filter. Adds a strict-mode regression test.
---------
Co-authored-by: Peter Rothlaender <peter.rothlaender@ginkgo.com>
* feat(admin-panel): add /api/admin/oauth/refresh endpoint for cross-origin BFF refresh
The cookie-based /api/auth/refresh controller can't be reached cross-origin
from a separately-hosted admin panel because the refresh-token cookie isn't
sent on cross-origin fetches. Add a dedicated POST /api/admin/oauth/refresh
endpoint that accepts the refresh token in the request body, exchanges it
at the IdP via openid-client refreshTokenGrant, and returns the same
response shape as /api/admin/oauth/exchange.
Implementation lives in packages/api/src/auth/refresh.ts as the
applyAdminRefresh helper. It validates the refreshed tokenset, looks up the
admin user by openidId (with optional user_id disambiguation when multiple
user docs share an openidId), mints the bearer via an injected mintToken
hook, and runs an optional onRefreshSuccess hook for downstream forks that
need to update server-side session state.
The default mintToken passed by the OSS route signs an HS256 LibreChat JWT
via generateToken so admin panel callers continue to use the existing local
JWT strategy. Forks that prefer to hand back an IdP-signed token (e.g. for
deployments where the JWT auth gate is JWKS-only) override mintToken
without changing the helper or the route.
Also threads expiresAt through AdminExchangeData and AdminExchangeResponse
so admin panel clients can drive proactive refresh before the bearer
expires. Defaults the OSS exchange flow to Date.now() + sessionExpiry.
* fix(admin-panel): address review feedback on /api/admin/oauth/refresh
mintToken now returns {token, expiresAt} so the minter is authoritative
for the bearer's lifetime instead of deriving it from the IdP `exp` claim.
The refresh response would otherwise lie to the admin panel and trigger
premature or late refresh cycles.
The helper now falls back to the inbound refresh_token when the IdP omits
one on rotation (Auth0 with rotation off, Microsoft personal accounts).
Without this the admin panel loses its refresh capability after one cycle.
Other hardening:
resolveAdminUser validates user_id with Types.ObjectId.isValid before
hitting Mongoose, avoiding a CastError that would surface as a generic
500 with no useful information for the client.
If user_id resolves to a user whose openidId does not match the refreshed
sub, throw USER_ID_MISMATCH (401) instead of silently swapping in a
different user matching the sub.
Wrap tokenset.claims() in readClaims so an IdP that returns a tokenset
without a usable id_token gets mapped to CLAIMS_INCOMPLETE (502) rather
than bubbling a raw exception.
findUsers now uses the same SAFE_USER_PROJECTION as getUserById so the
fallback path no longer pulls password/totpSecret/backupCodes into memory.
Removed dead fields (email on AdminRefreshClaims, id_token on
RefreshTokenset) and fixed import ordering per AGENTS.md.
Adds packages/api/src/auth/refresh.spec.ts: 18 tests covering the happy
path, userId disambiguation (match, invalid ObjectId, null, mismatch),
all error branches (IDP_INCOMPLETE, CLAIMS_INCOMPLETE for both throw and
missing sub, USER_NOT_FOUND, mintToken/onRefreshSuccess propagation), and
refresh-token preservation under rotation/no-rotation.
* chore(admin-panel): polish per re-review on /api/admin/oauth/refresh
readClaims now logs the original error name/message at warn before mapping
to CLAIMS_INCOMPLETE so a programming bug doesn't get silently rebadged
as an IdP problem in production logs.
The route handler's JSDoc now enumerates every error response (status +
error_code) so admin-panel implementors can plan for each branch without
reading the source.
Tightens the helper's surface: removed the now-dead `exp` field from
`AdminRefreshClaims` (only `sub` is read since the v2 mintToken refactor),
and tightened `AdminRefreshDeps.findUsers`'s projection parameter from
`string | null` to `string` so the contract matches actual usage.
Test polish: the userId-resolves-to-null fallthrough test now asserts the
exact `findUsers` and `getUserById` call arguments so a regression in the
fallthrough query shape is caught. The "skips onRefreshSuccess" test now
asserts a populated response shape rather than just `toBeDefined`.
Declined per prior triage and re-confirmed: a role guard inside
`applyAdminRefresh` (downstream `/api/admin/*` already enforces
ACCESS_ADMIN via requireCapability) and moving the IdP grant call out of
the JS route into TypeScript (matches existing oauth.js / openidStrategy
pattern; package-boundary refactor belongs in a separate PR).
* fix(admin-panel): reject /api/admin/oauth/refresh tokensets from foreign issuers
When the route handler can resolve the configured OpenID issuer, it now
threads it into applyAdminRefresh as expectedIssuer. The helper compares
that against the tokenset claims iss (after normalizeOpenIdIssuer on
both sides to absorb trailing-slash differences) and throws
ISSUER_MISMATCH (401) on mismatch.
The check is skipped when either side is unset so behavior is unchanged
for IdPs that don't return iss on a refresh-grant id_token, and for
older deployments where the OpenID config doesn't expose serverMetadata.
This is a defense-in-depth measure for the refresh path only. The
deeper OIDC posture fix (binding IUser lookup to (sub, iss) as a pair)
is pre-existing debt across openidStrategy.js and the regular exchange
flow as well, and belongs in a separate PR with the schema change and
backfill migration.
* fix(admin-panel): bind refresh user lookup to (sub, iss) and handle getOpenIdConfig throw
Two fixes raised on the PR thread that I previously misdescribed:
The user lookup in resolveAdminUser was keyed on openidId alone, so a
tokenset from a different issuer that happened to share the same sub
could resolve to a local user from a different IdP. Now exports
getIssuerBoundConditions and isUserIssuerAllowed from openid.ts (the
helpers findOpenIDUser already uses) and reuses them. The findUsers
filter becomes ($or of getIssuerBoundConditions for openidId) when an
expectedIssuer is provided, with the same legacy backward-compat
clause for users whose openidIssuer field was never populated. The
direct user_id path now also checks isUserIssuerAllowed and throws
USER_ID_MISMATCH if the stored openidIssuer disagrees with the
configured issuer.
The route's getOpenIdConfig() call was previously documented as
returning null when uninitialized; the actual implementation throws.
That made the if (!openIdConfig) guard unreachable, and an unconfigured
server would surface as 500 INTERNAL_ERROR rather than 503
OPENID_NOT_CONFIGURED. Wraps the call in try/catch so the documented
503 response is what callers actually receive.
Adds 4 tests covering the new lookup binding behavior.
* fix(admin-panel): re-check ACCESS_ADMIN on /api/admin/oauth/refresh
The IdP refresh token can outlive a capability/role change, so the
initial requireAdminAccess on the OAuth callback isn't sufficient.
Inject canAccessAdmin via the existing capability model
(hasCapability with SystemCapabilities.ACCESS_ADMIN, matching
requireAdminAccess so custom roles and user grants are honored)
and gate token minting on it. Capability backend errors are
warn-and-denied to keep the bearer-mint path fail-closed.
* fix(admin-panel): scope /api/admin/oauth/refresh to the request tenant
The same (openidId, openidIssuer) pair is allowed across tenants by
the user schema's unique index. The refresh helper was wrapping both
the direct getUserById and the fallback findUsers in runAsSystem,
bypassing tenant isolation, so an IdP identity that exists in two
tenants could resolve to the wrong tenant's user and mint a JWT
bound to that tenant.
Drop the runAsSystem wrappers, add a trusted tenantId option to
applyAdminRefresh, AND it into the fallback findUsers filter, and
assert it against the direct getUserById result. Mount
preAuthTenantMiddleware on the refresh route so the deployment's
X-Tenant-Id header drives the trusted tenant via ALS. Single-tenant
deploys (no header) keep the existing openidId-only behaviour.
Adds TENANT_MISMATCH (401) and a regression covering duplicate
(sub, iss) across tenants plus the direct-userId tenant assertion.
* fix(admin-panel): gate /api/admin/oauth/refresh on OPENID_REUSE_TOKENS
The OSS refreshController only refreshes OpenID tokensets when
OPENID_REUSE_TOKENS is enabled. The body-based admin variant was
unconditionally calling refreshTokenGrant, which made the flag
ineffective for the admin OAuth flow and let admin sessions keep
renewing in deployments that explicitly turned token reuse off.
Add the same isEnabled(process.env.OPENID_REUSE_TOKENS) check up
front and return 403 TOKEN_REUSE_DISABLED so the admin panel BFF
can surface the configuration mismatch instead of silently churning
through retries.
* 🪟 feat: Add allowedAddresses Exemption List For SSRF-Guarded Targets
LibreChat already blocks SSRF-prone targets (private IPs, loopback,
link-local, .internal/.local TLDs) at every server-side fetch site
that consumes user-controllable URLs — custom-endpoint baseURLs, MCP
servers, OpenAPI Actions, and OAuth endpoints. The only existing
escape hatch is `allowedDomains`, but that flips the field into a
strict whitelist: adding `127.0.0.1` to permit a self-hosted Ollama
also blocks every public destination that isn't in the list.
Introduce `allowedAddresses` as the orthogonal primitive: a private-
IP-space exemption list. When a hostname or its resolved IP appears
in the list, the SSRF block is bypassed for that target. Public
destinations remain reachable. Operators can now run self-hosted
LLMs / MCP servers / Action endpoints on private addresses without
weakening the default-deny posture for everything else.
Schema additions in `packages/data-provider/src/config.ts`:
- `endpoints.allowedAddresses` (new — gates `validateEndpointURL`)
- `mcpSettings.allowedAddresses` (parallel to `allowedDomains`)
- `actions.allowedAddresses` (parallel to `allowedDomains`)
Core changes in `packages/api/src/auth/`:
- New `isAddressAllowed(hostnameOrIP, allowedAddresses)` — pure,
case-insensitive, bracket-stripped literal match.
- Threaded the list through `isSSRFTarget`, `resolveHostnameSSRF`,
`isDomainAllowedCore`, `isActionDomainAllowed`, `isMCPDomainAllowed`,
`isOAuthUrlAllowed`, and `validateEndpointURL`.
- Extended `createSSRFSafeAgents` and `createSSRFSafeUndiciConnect`
to accept the list, building an SSRF-safe DNS lookup that exempts
matching hostnames/IPs at TCP connect time (TOCTOU-safe).
Wiring:
- Custom and OpenAI endpoint initialize sites pass
`endpoints.allowedAddresses` to `validateEndpointURL`.
- `MCPServersRegistry` stores `allowedAddresses` and exposes it via
`getAllowedAddresses()`. The factory, connection class, manager,
`UserConnectionManager`, and `ConnectionsRepository` all thread
it through to the SSRF utilities.
- `MCPOAuthHandler.initiateOAuthFlow`, `refreshOAuthTokens`, and
`validateOAuthUrl` accept the list and consult it on every URL
validation along the OAuth chain.
- `ToolService`, `ActionService`, and the assistants/agents action
routes pass `actions.allowedAddresses` to `isActionDomainAllowed`
and to `createSSRFSafeAgents` for runtime action calls.
- `initializeMCPs.js` reads `mcpSettings.allowedAddresses` from the
app config and forwards it to the registry constructor.
Documentation:
- `librechat.example.yaml` shows the new field next to each existing
`allowedDomains` block, with a note clarifying that
`allowedAddresses` is an exemption list (not a whitelist).
Tests:
- Unit tests for `isAddressAllowed` covering literal IPs, hostnames,
IPv6 brackets, case insensitivity, and partial-match rejection.
- Exemption tests for every entry point: `isSSRFTarget`,
`resolveHostnameSSRF`, `validateEndpointURL`, `isActionDomainAllowed`,
`isMCPDomainAllowed`, `isOAuthUrlAllowed`.
- Existing tests updated to reflect the new optional parameter.
Default behavior is unchanged: omitted = empty list = no exemptions.
* 🩹 fix: Plumb allowedAddresses Through AppConfig endpoints Type
The initial PR added `endpoints.allowedAddresses` to the
data-provider config schema and consumed it in the endpoint
initialize sites, but the runtime `AppConfig.endpoints` shape in
`@librechat/data-schemas` was a hand-maintained subset that didn't
include the new field — so `tsc` rejected `appConfig.endpoints.allowedAddresses`.
Add the field to `AppConfig['endpoints']` in
`packages/data-schemas/src/types/app.ts` and forward it from the
loaded config in `packages/data-schemas/src/app/endpoints.ts` so the
runtime config carries the value.
Update `initializeMCPs.spec.js` to expect the third positional
argument (`allowedAddresses`) on the `createMCPServersRegistry` call.
* 🩹 fix: Enforce allowedDomains Before allowedAddresses In isOAuthUrlAllowed
The initial implementation checked the address exemption first, so a
URL whose hostname appeared in `allowedAddresses` would return true
even when the admin had configured `allowedDomains` as a strict bound
on OAuth endpoints. A malicious MCP server could advertise OAuth
metadata, token, or revocation URLs at any address the admin had
permitted for an unrelated reason (a self-hosted LLM at `127.0.0.1`,
for example) and pass validation, expanding SSRF reach beyond the
configured domain whitelist.
Reorder: when `allowedDomains` is set, treat it as authoritative —
return true only if the URL matches a domain entry, otherwise fall
through to false. The address exemption only applies when no
`allowedDomains` is configured (mirrors how the downstream SSRF check
in `validateOAuthUrl` consults `allowedAddresses`).
Add a regression test asserting that an `allowedAddresses` entry does
not broaden a configured `allowedDomains` list.
Reported by chatgpt-codex-connector on PR #12933.
* 🩹 fix: Forward allowedAddresses To Remaining OAuth Callers
Two `MCPOAuthHandler` callers still used the pre-feature signatures and
were silently dropping the new `allowedAddresses` argument:
- `api/server/routes/mcp.js` invoked `initiateOAuthFlow` with the old
5-argument shape, so OAuth flows initiated through the route handler
ignored the registry's `getAllowedAddresses()` and would reject any
metadata/authorization/token URL on a permitted private host.
- `api/server/controllers/UserController.js#maybeUninstallOAuthMCP`
invoked `revokeOAuthToken` without the address exemption, so
uninstalling an OAuth-backed MCP server on a permitted private host
would fail at the revocation step even though the rest of the MCP
connection path now permits it.
Both sites now read `allowedAddresses` from the registry alongside
`allowedDomains` and forward it. Reported by Copilot on PR #12933.
* 🩹 fix: Update Test Mocks And Assertions For OAuth allowedAddresses
The previous commit started passing `allowedAddresses` to
`MCPOAuthHandler.initiateOAuthFlow` from `api/server/routes/mcp.js`
and to `MCPOAuthHandler.revokeOAuthToken` from
`api/server/controllers/UserController.js`, but the corresponding
test files mocked the registry without `getAllowedAddresses` (causing
`TypeError`s) and asserted the old positional shape on
`toHaveBeenCalledWith`.
Update the mocks and assertions to match the new arity:
- `api/server/routes/__tests__/mcp.spec.js`: add
`getAllowedDomains`/`getAllowedAddresses` to the registry mock and
expect the additional positional args on `initiateOAuthFlow`.
- `api/server/controllers/__tests__/maybeUninstallOAuthMCP.spec.js`:
add a `getAllowedAddresses` mock alongside the existing
`getAllowedDomains` and seed it in `setupOAuthServerFound`.
- `api/server/controllers/__tests__/UserController.mcpOAuth.spec.js`:
add `getAllowedAddresses` to the registry mock and expect the
trailing `null` arg on the three `revokeOAuthToken` assertions.
* 🛡️ fix: Address Comprehensive Review — Scope allowedAddresses To Private IP Space
Major findings from the comprehensive PR review (severity → fix):
**CRITICAL — `validateOAuthUrl` SSRF fallback bypass.** When `allowedDomains`
is configured and a URL fails the whitelist, the SSRF fallback in
`validateOAuthUrl` was still passing `allowedAddresses` to `isSSRFTarget` /
`resolveHostnameSSRF`, letting a malicious MCP server advertise OAuth
endpoints at any address the admin had permitted for an unrelated reason.
Suppress `allowedAddresses` in the fallback when `allowedDomains` is active —
the address exemption is opt-in for the no-whitelist mode only.
**MAJOR — WebSocket transport SSRF check ignored exemptions.** The
`constructTransport` WebSocket branch called `resolveHostnameSSRF(wsHostname)`
without `this.allowedAddresses`, so a permitted private MCP server would
pass `isMCPDomainAllowed` but be blocked at transport creation. Forward
the exemption.
**Scope `allowedAddresses` to private IP space only (operator directive).**
The exemption list is for permitting private/internal targets; it must not
be a back-door to broaden trust to public destinations.
- Schema (`packages/data-provider/src/config.ts`): new
`allowedAddressesSchema` rejects URLs (`://`), paths/CIDR (`/`),
whitespace, and public IPv4/IPv6 literals at config-load time. Wired
into `endpoints`, `mcpSettings`, and `actions`.
- Runtime (`packages/api/src/auth/domain.ts`): `isAddressAllowed` now
drops public-IP candidates and public-IP entries on the match path —
defense in depth so a misconfigured runtime list never grants exemption.
- Hot path (`packages/api/src/auth/agent.ts`): `buildSSRFSafeLookup`
pre-normalizes the list into a `Set<string>` once at construction and
applies the same scoping filter, so the connect-time DNS lookup is an
O(1) Set membership check instead of a full re-iterate-and-normalize on
every outbound request.
**Test coverage for the connect-time and OAuth-fallback paths.**
- `agent.spec.ts`: new describe block exercising `buildSSRFSafeLookup` and
`createSSRFSafe*` with `allowedAddresses` — hostname-literal exemption,
resolved-IP exemption, public-IP scoping, URL/CIDR/whitespace rejection,
and the default no-list block.
- `handler.allowedAddresses.test.ts` (new): integration tests for
`validateOAuthUrl` — covers both the no-domains-set "permit private"
path and the strict-bound regression where `allowedAddresses` must NOT
bypass `allowedDomains`.
**Documentation & cleanup.**
- `connection.ts` redirect SSRF check: explicit comment that
`allowedAddresses` is intentionally NOT consulted for redirect targets
(server-controlled, must not inherit the admin's exemption).
- `MCPConnectionFactory.test.ts`: replaced an `eslint-disable` with a
proper `import { getTenantId } from '@librechat/data-schemas'`. The
disable was added to make a pre-existing `require()` quiet — the cleaner
fix is to use the existing top-level import.
Updated `MCPConnectionSSRF.test.ts` WebSocket SSRF assertions to match the
new two-argument call shape (`hostname, allowedAddresses`).
* 🩹 fix: Require Absolute URL Before allowedAddresses Trust Bypass In isOAuthUrlAllowed
`parseDomainSpec` is lenient — it silently prepends `https://` to
schemeless inputs so it can match patterns like bare `example.com`.
That leniency leaked into `isOAuthUrlAllowed`'s new
`allowedAddresses` short-circuit: a value like `10.0.0.5/oauth` (no
scheme) would parse successfully via the prepended default, hit the
address-exemption path, return `true`, and skip `validateOAuthUrl`'s
strict `new URL(url)` parse-or-throw — only to fail later in OAuth
discovery with a less clear runtime error.
Add a strict `new URL(url)` gate at the top of `isOAuthUrlAllowed`.
Schemeless inputs now fall through to `validateOAuthUrl`'s explicit
"Invalid OAuth <field>" rejection. Tests added in both
`auth/domain.spec.ts` (unit) and the OAuth handler integration spec
(end-to-end).
Reported by chatgpt-codex-connector (P2) on PR #12933.
* 🛡️ fix: Address Follow-Up Comprehensive Review — Schema Tests, Shared Normalization, host:port
Auditing the second comprehensive review:
**F1 MAJOR — schema validation untested.** `allowedAddressesSchema` had
zero coverage, so a regression in the three refinement stages or the
three wiring locations (`endpoints` / `mcpSettings` / `actions`) would
silently let invalid entries reach the runtime. Added a dedicated
`describe('allowedAddressesSchema')` block in `config.spec.ts` covering:
valid private IPs (v4 + v6, including the previously-missed 192.0.0.0/24
range), accepted hostnames, all rejection categories (URLs, CIDR, paths,
whitespace tabs/newlines, host:port, public IP literals), and full
`configSchema.parse()` integration at each of the three nesting points.
**F2 MINOR — `isPrivateIPv4Literal` divergence.** The schema reimpl in
`packages/data-provider` was discarding the `c` octet, so the
`192.0.0.0/24` (RFC 5736 IETF protocol assignments) range that the
authoritative `isPrivateIPv4` accepts was being rejected with a
misleading "public IP" error. Destructure `c` and add the missing range
check; covered by the new schema tests.
**F3 MINOR — DRY violation across `domain.ts` and `agent.ts`.** Both
files had independent normalization implementations with a subtle
whitespace-check divergence (`/\s/` vs `.includes(' ')`). Extracted the
shared logic into a new `packages/api/src/auth/allowedAddresses.ts`
module that both consumers import:
- `normalizeAddressEntry(entry)` — single-entry shape check
- `looksLikeHostPort(entry)` — host:port detector (used by F4)
- `normalizeAllowedAddressesSet(list)` — pre-normalized Set for the
connect-time hot path
- `isAddressInAllowedSet(candidate, set)` — membership check that
enforces private-IP scoping on the candidate
Both `isAddressAllowed` (preflight) and `buildSSRFSafeLookup` (connect)
now go through the same primitives; the whitespace divergence is gone.
To break the import cycle (`allowedAddresses` needs `isPrivateIP`,
`domain` previously owned it), extracted IP private-range detection
into a leaf `auth/ip.ts` module. `domain.ts` re-exports `isPrivateIP`
for backward compatibility with existing call sites.
**F4 MINOR — `host:port` silently misclassified.** Entries like
`localhost:8080` previously slipped through the URL/path guard, were
mis-detected as IPv6, failed `isPrivateIP`, and were silently dropped
with a misleading "public IP" schema error. Added an explicit
`looksLikeHostPort` check with a clear error: "allowedAddresses
entries must not include a port — list the bare hostname or IP only."
Bare `::1`, `[::1]`, and other valid IPv6 literals are intentionally
not matched (regex distinguishes by colon count and the bracketed
`[ipv6]:port` form).
**F5 MINOR — hostname-trust documentation gap.** Hostname entries
short-circuit `resolveHostnameSSRF` before any DNS lookup — that's a
deliberate design (admin trusts the name) but it means the exemption
follows whatever the name resolves to at runtime. Added an explicit
note in `librechat.example.yaml` for both `mcpSettings.allowedAddresses`
and `endpoints.allowedAddresses`: "a hostname entry trusts whatever IP
that name resolves to. Only list hostnames whose DNS you control.
Prefer literal IPs when you can."
**F6** (8 positional params) is flagged for follow-up; refactor to an
options object is a breaking-API change deferred to a separate PR.
**F7** (redirect/WebSocket asymmetry, NIT, conf 40) — skipping; the
existing inline comment is sufficient.
* 🧹 chore: Address Follow-Up NITs — Import Order And Mirror-Function Naming
Three NITs from the latest comprehensive review:
**NIT #1 (conf 85) — local import order.** AGENTS.md requires local
imports sorted longest-to-shortest. Both `domain.ts` and `agent.ts`
had `./ip` (shorter) before `./allowedAddresses` (longer). Swapped.
**NIT #2 (conf 60) — missing cross-reference.** The schema-side
`isHostPortShape` in `packages/data-provider/src/config.ts` had no
note pointing at the canonical runtime mirror. Added a JSDoc paragraph
explaining the mirror relationship and why a local copy exists (the
data-provider package can't import from `@librechat/api` without
creating a circular dependency).
**NIT #3 (conf 50) — naming inconsistency.** Renamed
`isHostPortShape` → `looksLikeHostPort` so the schema mirror matches
the runtime helper exactly. Kept as a separate function (not a shared
import) for the same circular-dependency reason; the matching name
makes it obvious they should stay in lockstep.
* 🔐 fix: Strip code_challenge from admin OAuth requests before Passport
openid-client v6's Passport Strategy uses `currentUrl.searchParams.size === 0`
to distinguish initial authorization requests from OAuth callbacks. The
admin-panel-specific `code_challenge` query parameter caused the strategy to
misclassify the request as a callback and return 401 Unauthorized.
* 🔐 fix: Strip code_challenge from admin OAuth requests before Passport
openid-client v6's Passport Strategy uses `currentUrl.searchParams.size === 0`
to distinguish initial authorization requests from OAuth callbacks. The
admin-panel-specific `code_challenge` query parameter caused the strategy to
misclassify the request as a callback and return 401 Unauthorized.
- Fix regex to handle `code_challenge` in any query position without producing
malformed URLs, and handle empty `code_challenge=` values (`[^&]*` vs `[^&]+`)
- Combine `storePkceChallenge` + `stripCodeChallenge` into a single
`storeAndStripChallenge` helper to enforce read-store-strip ordering
- Apply defensively to all 7 admin OAuth providers
- Add 12 unit tests covering stripCodeChallenge and storeAndStripChallenge
* refactor: Extract PKCE helpers to utility file, harden tests
- Move stripCodeChallenge and storeAndStripChallenge to
api/server/utils/adminPkce.js — eliminates _test production export
and avoids loading the full auth.js module tree in tests
- Add missing req.originalUrl/req.url assertions to invalid-challenge
and no-challenge test branches (regression blind spots)
- Hoist cache reference to module scope in tests (was redundantly
re-acquired from mock factory on every beforeEach)
* chore: Address review NITs — imports, exports, naming, assertions
- Fix import order in auth.js (longest-to-shortest per CLAUDE.md)
- Remove unused PKCE_CHALLENGE_TTL/PKCE_CHALLENGE_PATTERN exports
- Hoist strip arrow to module-scope stripChallengeFromUrl
- Rename auth.test.js → auth.spec.js (project convention)
- Tighten cache-failure test: toBe instead of toContain, add req.url
* refactor: Move PKCE helpers to packages/api with dependency injection
Move stripCodeChallenge and storeAndStripChallenge from api/server/utils
into packages/api/src/auth/exchange.ts alongside the existing PKCE
verification logic. Cache is now injected as a Keyv parameter, matching
the dependency-injection pattern used throughout packages/api/.
- Add PkceStrippableRequest interface for minimal req typing
- auth.js imports storeAndStripChallenge from @librechat/api
- Delete api/server/utils/adminPkce.js
- Move tests to packages/api/src/auth/adminPkce.spec.ts (TypeScript,
real Keyv instances, no getLogStores mock needed)
* fix(auth): add origin binding to admin OAuth exchange codes
Bind admin OAuth exchange codes to the admin panel's origin at
generation time and validate the origin on redemption. This prevents
an intercepted code (via referrer leakage, logs, or network capture)
from being redeemed by a different origin within the 30-second TTL.
- Store the admin panel origin alongside the exchange code in cache
- Extract the request origin (from Origin/Referer headers) on the
exchange endpoint and pass it for validation
- Reject code redemption when the request origin does not match the
stored origin (code is still consumed to prevent replay)
- Backward compatible: codes without a stored origin are accepted
* fix(auth): add PKCE proof-of-possession to admin OAuth exchange codes
Add a PKCE-like code_challenge/code_verifier flow to the admin OAuth
exchange so that intercepting the exchange code alone is insufficient
to redeem it. The admin panel generates a code_verifier (stored in its
HttpOnly session cookie) and sends sha256(verifier) as code_challenge
through the OAuth initiation URL. LibreChat stores the challenge keyed
by OAuth state and attaches it to the exchange code. On redemption, the
admin panel sends the verifier and LibreChat verifies the hash match.
- Add verifyCodeChallenge() helper using SHA-256
- Store code_challenge in ADMIN_OAUTH_EXCHANGE cache (pkce: prefix, 5min TTL)
- Capture OAuth state in callback middleware before passport processes it
- Accept code_verifier in exchange endpoint body
- Backward compatible: no challenge stored → PKCE check skipped
* fix(auth): harden PKCE and origin binding in admin OAuth exchange
- Await cache.set for PKCE challenge storage with error handling
- Use crypto.timingSafeEqual for PKCE hash comparison
- Drop case-insensitive flag from hex validation regexes
- Add code_verifier length validation (max 512 chars)
- Normalize Origin header via URL parsing in resolveRequestOrigin
- Add test for undefined requestOrigin rejection
- Clarify JSDoc: hex-encoded SHA-256, not RFC 7636 S256
* fix(auth): fail closed on PKCE callback cache errors, clean up origin/buffer handling
- Callback middleware now redirects to error URL on cache.get failure
instead of silently continuing without PKCE challenge
- resolveRequestOrigin returns undefined (not raw header) on parse failure
- Remove dead try/catch around Buffer.from which never throws for string input
* chore(auth): remove narration comments, scope eslint-disable to lines
* chore(auth): narrow query.state to string, remove narration comments in exchange.ts
* fix(auth): address review findings — warn on missing PKCE challenge, validate verifier length, deduplicate URL parse
- Log warning when OAuth state is present but no PKCE challenge found
- Add minimum length check (>= 1) on code_verifier input validation
- Update POST /oauth/exchange JSDoc to document code_verifier param
- Deduplicate new URL(redirectUri) parse in createOAuthHandler
- Restore intent comment on pre-delete pattern in exchangeAdminCode
* test(auth): replace mock cache with real Keyv, remove all as-any casts
- Use real Keyv in-memory store instead of hand-rolled Map mock
- Replace jest.fn mocks with jest.spyOn on real Keyv instance
- Remove redundant store.has() assertion, use cache.get() instead
- Eliminate all eslint-disable and as-any suppressions
- User fixture no longer needs any cast (Keyv accepts plain objects)
* fix(auth): add IUser type cast for test fixture to satisfy tsc
* fix(data-schemas): resolve TypeScript strict type check errors in source files
- Constrain ConfigSection to string keys via `string & keyof TCustomConfig`
- Replace broken `z` import from data-provider with TCustomConfig derivation
- Add `_id: Types.ObjectId` to IUser matching other Document interfaces
- Add `federatedTokens` and `openidTokens` optional fields to IUser
- Type mongoose model accessors as `Model<IRole>` and `Model<IUser>`
- Widen `getPremiumRate` param to accept `number | null`
- Widen `bulkWriteAclEntries` ops to untyped `AnyBulkWriteOperation[]`
- Fix `getUserPrincipals` return type to use `PrincipalType` enum
- Add non-null assertions for `connection.db` in migration files
- Import DailyRotateFile constructor directly instead of relying on
broken module augmentation across mismatched node_modules trees
- Add winston-daily-rotate-file as devDependency for type resolution
* fix(data-schemas): resolve TypeScript type errors in test files
- Replace arbitrary test keys with valid TCustomConfig properties in config.spec
- Use non-null assertions for permission objects in role.methods.spec
- Replace `.SHARED_GLOBAL` access with `.not.toHaveProperty()` for legacy field
- Add non-null assertions for balance, writeRate, readRate in spendTokens.spec
- Update mock user _id to use ObjectId in user.test
- Remove unused Schema import in tenantIndexes.spec
* fix(api): resolve TypeScript strict type check errors across source and test files
- Widen getUserPrincipals dep type in capabilities middleware
- Fix federatedTokens type in createSafeUser return
- Use proper mock req type for read-only properties in preAuthTenant.spec
- Replace `as IUser` casts with ObjectId-typed mocks in openid/oidc specs
- Use TokenExchangeMethodEnum values instead of string literals in MCP specs
- Fix SessionStore type compatibility in sessionCache specs
- Replace `catch (error: any)` with `(error as Error)` in redis specs
- Remove invalid properties from test data in initialize and MCP specs
- Add String.prototype.isWellFormed declaration for sanitizeTitle spec
* fix(client): resolve TypeScript type errors in shared client components
- Add default values for destructured bindings in OGDialogTemplate
- Replace broken ExtendedFile import with inline type in FileIcon
* ci: add TypeScript type-check job to backend review workflow
Add a `typecheck` job that runs `tsc --noEmit` on all four TypeScript
workspaces (data-provider, data-schemas, @librechat/api, @librechat/client)
after the build step. Catches type errors that rollup builds may miss.
* fix(data-schemas): add local type declaration for DailyRotateFile transport
The `winston-daily-rotate-file` package ships a module augmentation for
`winston/lib/winston/transports`, but it fails when winston and
winston-daily-rotate-file resolve from different node_modules trees
(which happens in this monorepo due to npm hoisting).
Add a local `.d.ts` declaration that augments the same module path from
within data-schemas' compilation unit, so `tsc --noEmit` passes while
keeping the original runtime pattern (`new winston.transports.DailyRotateFile`).
* fix: address code review findings from PR #12451
- Restore typed `AnyBulkWriteOperation<AclEntry>[]` on bulkWriteAclEntries,
cast to untyped only at the tenantSafeBulkWrite call site (Finding 1)
- Type `findUser` model accessor consistently with `findUsers` (Finding 2)
- Replace inline `import('mongoose').ClientSession` with top-level import type
- Use `toHaveLength` for spy assertions in playwright-expect spec file
- Replace numbered Record casts with `.not.toHaveProperty()` in
role.methods.spec for SHARED_GLOBAL assertions
- Use per-test ObjectIds instead of shared testUserId in openid.spec
- Replace inline `import()` type annotations with top-level SessionData
import in sessionCache spec
- Remove extraneous blank line in user.ts searchUsers
* refactor: address remaining review findings (4–7)
- Extract OIDCTokens interface in user.ts; deduplicate across IUser fields
and oidc.ts FederatedTokens (Finding 4)
- Move String.isWellFormed declaration from spec file to project-level
src/types/es2024-string.d.ts (Finding 5)
- Replace verbose `= undefined` defaults in OGDialogTemplate with null
coalescing pattern (Finding 6)
- Replace `Record<string, unknown>` TestConfig with named interface
containing explicit test fields (Finding 7)
* chore: move database model methods to /packages/data-schemas
* chore: add TypeScript ESLint rule to warn on unused variables
* refactor: model imports to streamline access
- Consolidated model imports across various files to improve code organization and reduce redundancy.
- Updated imports for models such as Assistant, Message, Conversation, and others to a unified import path.
- Adjusted middleware and service files to reflect the new import structure, ensuring functionality remains intact.
- Enhanced test files to align with the new import paths, maintaining test coverage and integrity.
* chore: migrate database models to packages/data-schemas and refactor all direct Mongoose Model usage outside of data-schemas
* test: update agent model mocks in unit tests
- Added `getAgent` mock to `client.test.js` to enhance test coverage for agent-related functionality.
- Removed redundant `getAgent` and `getAgents` mocks from `openai.spec.js` and `responses.unit.spec.js` to streamline test setup and reduce duplication.
- Ensured consistency in agent mock implementations across test files.
* fix: update types in data-schemas
* refactor: enhance type definitions in transaction and spending methods
- Updated type definitions in `checkBalance.ts` to use specific request and response types.
- Refined `spendTokens.ts` to utilize a new `SpendTxData` interface for better clarity and type safety.
- Improved transaction handling in `transaction.ts` by introducing `TransactionResult` and `TxData` interfaces, ensuring consistent data structures across methods.
- Adjusted unit tests in `transaction.spec.ts` to accommodate new type definitions and enhance robustness.
* refactor: streamline model imports and enhance code organization
- Consolidated model imports across various controllers and services to a unified import path, improving code clarity and reducing redundancy.
- Updated multiple files to reflect the new import structure, ensuring all functionalities remain intact.
- Enhanced overall code organization by removing duplicate import statements and optimizing the usage of model methods.
* feat: implement loadAddedAgent and refactor agent loading logic
- Introduced `loadAddedAgent` function to handle loading agents from added conversations, supporting multi-convo parallel execution.
- Created a new `load.ts` file to encapsulate agent loading functionalities, including `loadEphemeralAgent` and `loadAgent`.
- Updated the `index.ts` file to export the new `load` module instead of the deprecated `loadAgent`.
- Enhanced type definitions and improved error handling in the agent loading process.
- Adjusted unit tests to reflect changes in the agent loading structure and ensure comprehensive coverage.
* refactor: enhance balance handling with new update interface
- Introduced `IBalanceUpdate` interface to streamline balance update operations across the codebase.
- Updated `upsertBalanceFields` method signatures in `balance.ts`, `transaction.ts`, and related tests to utilize the new interface for improved type safety.
- Adjusted type imports in `balance.spec.ts` to include `IBalanceUpdate`, ensuring consistency in balance management functionalities.
- Enhanced overall code clarity and maintainability by refining type definitions related to balance operations.
* feat: add unit tests for loadAgent functionality and enhance agent loading logic
- Introduced comprehensive unit tests for the `loadAgent` function, covering various scenarios including null and empty agent IDs, loading of ephemeral agents, and permission checks.
- Enhanced the `initializeClient` function by moving `getConvoFiles` to the correct position in the database method exports, ensuring proper functionality.
- Improved test coverage for agent loading, including handling of non-existent agents and user permissions.
* chore: reorder memory method exports for consistency
- Moved `deleteAllUserMemories` to the correct position in the exported memory methods, ensuring a consistent and logical order of method exports in `memory.ts`.
* 🔐 fix: Reject OpenID email fallback when stored openidId mismatches token sub
When `findOpenIDUser` falls back to email lookup after the primary
`openidId`/`idOnTheSource` query fails, it now rejects any user whose
stored `openidId` differs from the incoming JWT subject claim. This
closes an account-takeover vector where a valid IdP JWT containing a
victim's email but a different `sub` could authenticate as the victim
when OPENID_REUSE_TOKENS is enabled.
The migration path (user has no `openidId` yet) is unaffected.
* test: Validate openidId mismatch guard in email fallback path
Update `findOpenIDUser` unit tests to assert that email-based lookups
returning a user with a different `openidId` are rejected with
AUTH_FAILED. Add matching integration test in `openIdJwtStrategy.spec`
exercising the full verify callback with the real `findOpenIDUser`.
* 🔐 fix: Remove redundant `openidId` truthiness check from mismatch guard
The `&& openidId` middle term in the guard condition caused it to be
bypassed when the incoming token `sub` was empty or undefined. Since
the JS callers can pass `payload?.sub` (which may be undefined), this
created a path where the guard never fired and the email fallback
returned the victim's account. Removing the term ensures the guard
rejects whenever the stored openidId differs from the incoming value,
regardless of whether the incoming value is falsy.
* test: Cover falsy openidId bypass and openidStrategy mismatch rejection
Add regression test for the guard bypass when `openidId` is an empty
string and the email lookup finds a user with a stored openidId.
Add integration test in openidStrategy.spec.js exercising the
mismatch rejection through the full processOpenIDAuth callback,
ensuring both OIDC paths (JWT reuse and standard callback) are
covered.
Restore intent-documenting comment on the no-provider fixture.
* fix: exempt allowedDomains from MCP OAuth SSRF checks (#12254)
The SSRF guard in validateOAuthUrl was context-blind — it blocked
private/internal OAuth endpoints even for admin-trusted MCP servers
listed in mcpSettings.allowedDomains. Add isHostnameAllowed() to
domain.ts and skip SSRF checks in validateOAuthUrl when the OAuth
endpoint hostname matches an allowed domain.
* refactor: thread allowedDomains through MCP connection stack
Pass allowedDomains from MCPServersRegistry through BasicConnectionOptions,
MCPConnectionFactory, and into MCPOAuthHandler method calls so the OAuth
layer can exempt admin-trusted domains from SSRF validation.
* test: add allowedDomains bypass tests and fix registry mocks
Add isHostnameAllowed unit tests (exact, wildcard, case-insensitive,
private IPs). Add MCPOAuthSecurity tests covering the allowedDomains
bypass for initiateOAuthFlow, refreshOAuthTokens, and revokeOAuthToken.
Update registry mocks to include getAllowedDomains.
* fix: enforce protocol/port constraints in OAuth allowedDomains bypass
Replace isHostnameAllowed (hostname-only check) with isOAuthUrlAllowed
which parses the full OAuth URL and matches against allowedDomains
entries including protocol and explicit port constraints — mirroring
isDomainAllowedCore's allowlist logic. Prevents a port-scoped entry
like 'https://auth.internal:8443' from also exempting other ports.
* test: cover auto-discovery and branch-3 refresh paths with allowedDomains
Add three new integration tests using a real OAuth test server:
- auto-discovered OAuth endpoints allowed when server IP is in allowedDomains
- auto-discovered endpoints rejected when allowedDomains doesn't match
- refreshOAuthTokens branch 3 (no clientInfo/config) with allowedDomains bypass
Also rename describe block from ephemeral issue number to durable name.
* docs: explain intentional absence of allowedDomains in completeOAuthFlow
Prevents future contributors from assuming a missing parameter during
security audits — URLs are pre-validated during initiateOAuthFlow.
* test: update initiateOAuthFlow assertion for allowedDomains parameter
* perf: avoid redundant URL parse for admin-trusted OAuth endpoints
Move isOAuthUrlAllowed check before the hostname extraction so
admin-trusted URLs short-circuit with a single URL parse instead
of two. The hostname extraction (new URL) is now deferred to the
SSRF-check path where it's actually needed.
* 🛡️ fix: Block SSRF via user-provided baseURL in endpoint initialization
User-provided baseURL values (when endpoint is configured with
`user_provided`) were passed through to the OpenAI SDK without
validation. Combined with `directEndpoint`, this allowed arbitrary
server-side requests to internal/metadata URLs.
Adds `validateEndpointURL` that checks against known SSRF targets
and DNS-resolves hostnames to block private IPs. Applied in both
custom and OpenAI endpoint initialization paths.
* 🧪 test: Add validateEndpointURL SSRF tests
Covers unparseable URLs, localhost, private IPs, link-local/metadata,
internal Docker/K8s hostnames, DNS resolution to private IPs, and
legitimate public URLs.
* 🛡️ fix: Add protocol enforcement and import order fix
- Reject non-HTTP/HTTPS schemes (ftp://, file://, data:, etc.) in
validateEndpointURL before SSRF hostname checks
- Document DNS rebinding limitation and fail-open semantics in JSDoc
- Fix import order in custom/initialize.ts per project conventions
* 🧪 test: Expand SSRF validation coverage and add initializer integration tests
Unit tests for validateEndpointURL:
- Non-HTTP/HTTPS schemes (ftp, file, data)
- IPv6 loopback, link-local, and unique-local addresses
- .local and .internal TLD hostnames
- DNS fail-open path (lookup failure allows request)
Integration tests for initializeCustom and initializeOpenAI:
- Guard fires when userProvidesURL is true
- Guard skipped when URL is system-defined or falsy
- SSRF rejection propagates and prevents getOpenAIConfig call
* 🐛 fix: Correct broken env restore in OpenAI initialize spec
process.env was captured by reference, not by value, making the
restore closure a no-op. Snapshot individual env keys before mutation
so they can be properly restored after each test.
* 🛡️ fix: Throw structured ErrorTypes for SSRF base URL validation
Replace plain-string Error throws in validateEndpointURL with
JSON-structured errors using type 'invalid_base_url' (matching new
ErrorTypes.INVALID_BASE_URL enum value). This ensures the client-side
Error component can look up a localized message instead of falling
through to the raw-text default.
Changes across workspaces:
- data-provider: add INVALID_BASE_URL to ErrorTypes enum
- packages/api: throwInvalidBaseURL helper emits structured JSON
- client: add errorMessages entry and localization key
- tests: add structured JSON format assertion
* 🧹 refactor: Use ErrorTypes enum key in Error.tsx for consistency
Replace bare string literal 'invalid_base_url' with computed property
[ErrorTypes.INVALID_BASE_URL] to match every other entry in the
errorMessages map.
* 🛡️ fix: Fail-closed MCP domain validation for unparseable URLs
`isMCPDomainAllowed` returned true (allow) when `extractMCPServerDomain`
could not parse the URL, treating it identically to a stdio transport.
A URL containing template placeholders or invalid syntax bypassed the
domain allowlist, then `processMCPEnv` resolved it to a valid—and
potentially disallowed—host at connection time.
Distinguish "no URL" (stdio, allowed) from "has URL but unparseable"
(rejected when an allowlist is active) by checking whether `config.url`
is an explicit non-empty string before falling through to the stdio path.
When no allowlist is configured the guard does not fire—unparseable URLs
fall through to connection-level SSRF protection via
`createSSRFSafeUndiciConnect`, preserving legitimate `customUserVars`
template-URL configs.
* test: Expand MCP domain validation coverage for invalid/templated URLs
Cover all branches of the fail-closed guard:
- Invalid/templated URLs rejected when allowlist is configured
- Invalid/templated URLs allowed when no allowlist (null/undefined/[])
- Whitespace-only and empty-string URLs treated as absent across all
allowedDomains configurations
- Stdio configs (no url property) remain allowed
* 🛡️ fix: Cover full fe80::/10 link-local range in SSRF IPv6 check
The `isPrivateIP` check used `startsWith('fe80')` which only matched
fe80:: but missed fe90::–febf:: (the rest of the RFC 4291 fe80::/10
link-local block). Replace with a proper bitwise hextet check.
* 🛡️ fix: Guard isIPv6LinkLocal against parseInt partial-parse on hostnames
parseInt('fe90.example.com', 16) stops at the dot and returns 0xfe90,
which passes the bitmask check and false-positives legitimate domains.
Add colon-presence guard (IPv6 literals always contain ':') and a hex
regex validation on the first hextet before parseInt.
Also document why fc/fd use startsWith while fe80::/10 needs bitwise.
* ✅ test: Harden IPv6 link-local SSRF tests with false-positive guards
- Assert fe90/fea0/febf hostnames are NOT blocked (regression guard)
- Add feb0::1 and bracket form [fe90::1] to isPrivateIP coverage
- Extend resolveHostnameSSRF tests for fe90::1 and febf::1
* 🔒 fix: handle hex-normalized IPv4-mapped IPv6 in domain validation
* fix: Enhance IPv6 private address detection in domain validation
- Added tests for detecting IPv4-compatible, 6to4, NAT64, and Teredo addresses.
- Implemented `extractEmbeddedIPv4` function to identify private IPv4 addresses within various IPv6 formats.
- Updated `isPrivateIP` function to utilize the new extraction logic for improved accuracy in address validation.
* fix: Update private IPv4 detection logic in domain validation
- Enhanced the `isPrivateIPv4` function to accurately identify additional private and non-routable IPv4 ranges.
- Adjusted the return logic in `resolveHostnameSSRF` to utilize the updated private IP detection for improved hostname validation.
* test: Expand private IP detection tests in domain validation
- Added tests for additional private IPv4 ranges including 0.0.0.0/8, 100.64.0.0/10, 192.0.0.0/24, and 198.18.0.0/15.
- Updated existing tests to ensure accurate detection of private and multicast IP addresses in the `isPrivateIP` function.
- Enhanced `resolveHostnameSSRF` to correctly identify private literal IPv4 addresses without DNS lookup.
* refactor: Rename and enhance embedded IPv4 detection in IPv6 addresses
- Renamed `extractEmbeddedIPv4` to `hasPrivateEmbeddedIPv4` for clarity on its purpose.
- Updated logic to accurately check for private IPv4 addresses embedded in Teredo, 6to4, and NAT64 IPv6 formats.
- Improved the `isPrivateIP` function to utilize the new naming and logic for better readability and accuracy.
- Enhanced documentation for clarity on the functionality of the updated methods.
* feat: Enhance private IPv4 detection in embedded IPv6 addresses
- Added additional checks in `hasPrivateEmbeddedIPv4` to ensure only valid private IPv4 formats are recognized.
- Improved the logic for identifying private IPv4 addresses embedded within various IPv6 formats, enhancing overall accuracy.
* test: Add additional test for hostname resolution in SSRF detection
- Included a new test case in `resolveHostnameSSRF` to validate the detection of private IPv4 addresses embedded in IPv6 formats for the hostname 'meta.example.com'.
- Enhanced existing tests to ensure comprehensive coverage of hostname resolution scenarios.
* fix: Set redirect option to 'manual' in undiciFetch calls
- Updated undiciFetch calls in MCPConnection to include the redirect option set to 'manual' for better control over HTTP redirects.
- Added documentation comments regarding SSRF pre-checks for WebSocket connections, highlighting the limitations of the current SDK regarding DNS resolution.
* test: Add integration tests for MCP SSRF protections
- Introduced a new test suite for MCP SSRF protections, verifying that MCPConnection does not follow HTTP redirects to private IPs and blocks WebSocket connections to private IPs when SSRF protection is enabled.
- Implemented tests to ensure correct behavior of the connection under various scenarios, including redirect handling and WebSocket DNS resolution.
* refactor: Improve SSRF protection logic for WebSocket connections
- Enhanced the SSRF pre-check for WebSocket connections to validate resolved IPs, ensuring that allowlisting a domain does not grant trust to its resolved IPs at runtime.
- Updated documentation comments to clarify the limitations of the current SDK regarding DNS resolution and the implications for SSRF protection.
* test: Enhance MCP SSRF protection tests for redirect handling and WebSocket connections
- Updated tests to ensure that MCPConnection does not follow HTTP redirects to private IPs, regardless of SSRF protection settings.
- Added checks to verify that WebSocket connections to hosts resolving to private IPs are blocked, even when SSRF protection is disabled.
- Improved documentation comments for clarity on the behavior of the tests and the implications for SSRF protection.
* test: Refactor MCP SSRF protection test for WebSocket connection errors
- Updated the test to use `await expect(...).rejects.not.toThrow(...)` for better readability and clarity.
- Simplified the error handling logic while ensuring that SSRF rejections are correctly validated during connection failures.
* refactor: better SSRF Protection in Action and Tool Services
- Added `createSSRFSafeAgents` function to create HTTP/HTTPS agents that block connections to private/reserved IP addresses, enhancing security against SSRF attacks.
- Updated `createActionTool` to accept a `useSSRFProtection` parameter, allowing the use of SSRF-safe agents during tool execution.
- Modified `processRequiredActions` and `loadAgentTools` to utilize the new SSRF protection feature based on allowed domains configuration.
- Introduced `resolveHostnameSSRF` function to validate resolved IPs against private ranges, preventing potential SSRF vulnerabilities.
- Enhanced tests for domain resolution and private IP detection to ensure robust SSRF protection mechanisms are in place.
* feat: Implement SSRF protection in MCP connections
- Added `createSSRFSafeUndiciConnect` function to provide SSRF-safe DNS lookup options for undici agents.
- Updated `MCPConnection`, `MCPConnectionFactory`, and `ConnectionsRepository` to include `useSSRFProtection` parameter, enabling SSRF protection based on server configuration.
- Enhanced `MCPManager` and `UserConnectionManager` to utilize SSRF protection when establishing connections.
- Updated tests to validate the integration of SSRF protection across various components, ensuring robust security measures are in place.
* refactor: WS MCPConnection with SSRF protection and async transport construction
- Added `resolveHostnameSSRF` to validate WebSocket hostnames against private IP addresses, enhancing SSRF protection.
- Updated `constructTransport` method to be asynchronous, ensuring proper handling of SSRF checks before establishing connections.
- Improved error handling for WebSocket transport to prevent connections to potentially unsafe addresses.
* test: Enhance ActionRequest tests for SSRF-safe agent passthrough
- Added tests to verify that httpAgent and httpsAgent are correctly passed to axios.create when provided in ActionRequest.
- Included scenarios to ensure agents are not included when no options are specified.
- Enhanced coverage for POST requests to confirm agent passthrough functionality.
- Improved overall test robustness for SSRF protection in ActionRequest execution.
* feat: implement admin authentication with OpenID & Local Auth proxy support
* feat: implement admin OAuth exchange flow with caching support
- Added caching for admin OAuth exchange codes with a short TTL.
- Introduced new endpoints for generating and exchanging admin OAuth codes.
- Updated relevant controllers and routes to handle admin panel redirects and token exchanges.
- Enhanced logging for better traceability of OAuth operations.
* refactor: enhance OpenID strategy mock to support multiple verify callbacks
- Updated the OpenID strategy mock to store and retrieve verify callbacks by strategy name.
- Improved backward compatibility by maintaining a method to get the last registered callback.
- Adjusted tests to utilize the new callback retrieval methods, ensuring clarity in the verification process for the 'openid' strategy.
* refactor: reorder import statements for better organization
* refactor: admin OAuth flow with improved URL handling and validation
- Added a utility function to retrieve the admin panel URL, defaulting to a local development URL if not set in the environment.
- Updated the OAuth exchange endpoint to include validation for the authorization code format.
- Refactored the admin panel redirect logic to handle URL parsing more robustly, ensuring accurate origin comparisons.
- Removed redundant local URL definitions from the codebase for better maintainability.
* refactor: remove deprecated requireAdmin middleware and migrate to TypeScript
- Deleted the old requireAdmin middleware file and its references in the middleware index.
- Introduced a new TypeScript version of the requireAdmin middleware with enhanced error handling and logging.
- Updated routes to utilize the new requireAdmin middleware, ensuring consistent access control for admin routes.
* feat: add requireAdmin middleware for admin role verification
- Introduced requireAdmin middleware to enforce admin role checks for authenticated users.
- Implemented comprehensive error handling and logging for unauthorized access attempts.
- Added unit tests to validate middleware functionality and ensure proper behavior for different user roles.
- Updated middleware index to include the new requireAdmin export.
* 🔒 fix: Enhance SSRF Protection and Domain Handling in MCP Server Configuration
- Updated the `extractMCPServerDomain` function to return the full origin (protocol://hostname:port) for improved protocol/port matching against allowed domains.
- Enhanced tests for `isMCPDomainAllowed` to validate domain access for internal hostnames and .local TLDs, ensuring proper SSRF protection.
- Added detailed comments in the configuration file to clarify security measures regarding allowed domains and internal target access.
* refactor: Domain Validation for WebSocket Protocols in Action and MCP Handling
- Added comprehensive tests to validate handling of WebSocket URLs in `isActionDomainAllowed` and `isMCPDomainAllowed` functions, ensuring that WebSocket protocols are rejected for OpenAPI Actions while allowed for MCP.
- Updated domain validation logic to support HTTP, HTTPS, WS, and WSS protocols, enhancing security and compliance with specifications.
- Refactored `parseDomainSpec` to improve protocol recognition and validation, ensuring robust handling of domain specifications.
- Introduced detailed comments to clarify the purpose and security implications of domain validation functions.
Addresses Server-Side Request Forgery vulnerability that allowed authenticated
attackers to interact with arbitrary internal/external HTTP services via the
Actions feature, including the internal RAG API, localhost services, and cloud
metadata endpoints.
## Security Changes
### 1. SSRF Target Blocklist (when allowedDomains is empty)
- Block localhost and loopback addresses (127.0.0.0/8, ::1)
- Block private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
- Block link-local/cloud metadata IPs (169.254.0.0/16)
- Block common internal Docker/Kubernetes service names (rag_api, redis, mongo, etc.)
- Block .internal and .local TLDs
Admins can still explicitly allow internal targets by adding them to
`actions.allowedDomains` in librechat.yaml.
### 2. Protocol and Port Restrictions
Enhanced `allowedDomains` to support protocol and port constraints:
- `example.com` - any protocol, any port (existing behavior)
- `https://example.com` - HTTPS only, any port
- `https://api.example.com:8443` - HTTPS only, port 8443 only
This allows admins to restrict actions to specific protocols/ports, preventing
attackers from accessing unintended services on allowed domains.
### 3. Redirect-based SSRF Prevention
Disabled automatic redirect following in action HTTP requests (`maxRedirects: 0`).
This prevents attackers from bypassing domain restrictions by:
1. Pointing action to allowed external domain
2. External domain redirects to internal service (e.g., 127.0.0.1)
3. Server follows redirect and accesses internal service
## Files Changed
- packages/api/src/auth/domain.ts: Added isSSRFTarget(), parseDomainSpec(),
updated isActionDomainAllowed() with protocol/port matching
- packages/api/src/auth/domain.spec.ts: Added tests for SSRF protection,
protocol restrictions, and port restrictions
- packages/data-provider/src/actions.ts: Added maxRedirects: 0 to axios config
## Configuration Example
# librechat.yaml
actions:
allowedDomains:
- "https://api.example.com" # HTTPS only
- "https://api.example.com:8443" # HTTPS + specific port
- "http://localhost:3000" # Admin override for local devRef: SBA-ADV-20251205-02
CVSSv3: 9.1 (Critical) - CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:L
* 🔒 feat: Add MCP server domain restrictions for remote transports
* 🔒 feat: Implement comprehensive MCP error handling and domain validation
- Added `handleMCPError` function to centralize error responses for domain restrictions and inspection failures.
- Introduced custom error classes: `MCPDomainNotAllowedError` and `MCPInspectionFailedError` for better error management.
- Updated MCP server controllers to utilize the new error handling mechanism.
- Enhanced domain validation logic in `createMCPTools` and `createMCPTool` functions to prevent operations on disallowed domains.
- Added tests for runtime domain validation scenarios to ensure correct behavior.
* chore: import order
* 🔒 feat: Enhance domain validation in MCP tools with user role-based restrictions
- Integrated `getAppConfig` to fetch allowed domains based on user roles in `createMCPTools` and `createMCPTool` functions.
- Removed the deprecated `getAllowedDomains` method from `MCPServersRegistry`.
- Updated tests to verify domain restrictions are applied correctly based on user roles.
- Ensured that domain validation logic is consistent and efficient across tool creation processes.
* 🔒 test: Refactor MCP tests to utilize configurable app settings
- Introduced a mock for `getAppConfig` to enhance test flexibility.
- Removed redundant mock definition to streamline test setup.
- Ensured tests are aligned with the latest domain validation logic.
---------
Co-authored-by: Atef Bellaaj <slalom.bellaaj@external.daimlertruck.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
- Updated the `findUser` method to normalize email fields to lowercase and trimmed whitespace for case-insensitive matching.
- Enhanced the `normalizeEmailInCriteria` function to handle email normalization in search criteria, including `` conditions.
- Added tests to ensure email normalization works correctly across various scenarios, including case differences and whitespace handling.
* 📫 refactor: Enhance OpenID email Fallback
* Updated email retrieval logic to use preferred_username or upn if email is not available.
* Adjusted logging and user data assignment to reflect the new email handling approach.
* Ensured email domain validation checks the correct email source.
* 🔄 refactor: Update Email Domain Validation Logic
* Modified `isEmailDomainAllowed` function to return true for falsy emails and missing domain restrictions.
* Added new test cases to cover scenarios with and without domain restrictions.
* Ensured proper validation when domain restrictions are present.
* chore: move domain related functions to `packages/api`
* fix: isEmailDomainAllowed for case-insensitive domain matching
- Added tests to validate case-insensitive matching for email domains in various scenarios.
- Updated isEmailDomainAllowed function to convert email domains to lowercase for consistent comparison.
- Improved handling of null/undefined entries in allowedDomains.
* ci: Mock isEmailDomainAllowed in samlStrategy tests
- Added a mock implementation for isEmailDomainAllowed to return true in samlStrategy tests, ensuring consistent behavior during test execution.
* ci: Update import of isEmailDomainAllowed in ldapStrategy tests
- Changed the import of isEmailDomainAllowed from the domains service to the api package for consistency and to reflect recent refactoring.
* add use of immutable claims to identify user object
* fix semicolons
* update email attribute on change
* replace ternary expressions
* fix semicolon
* chore: add typing
* chore: reorder fields in `findOpenIDUser`
* refactor: optimize user lookup logic in `findOpenIDUser` function to minimize database roundtrips
* refactor: integrate findOpenIDUser for improved user retrieval in refreshController
* refactor: improve error logging for invalid refresh tokens in refreshController
* ci: mock findUser correctly in openidStrategy tests
* test: add unit tests for findOpenIDUser function to enhance user retrieval logic
---------
Co-authored-by: Joachim Keltsch <joachim.keltsch@daimlertruck.com>
* 🔧 fix: Enhance OpenID token handling with user ID for image path validation
* 🔧 fix: Change logger level to error for user info fetch failure and remove redundant info log in OpenID user lookup
* 🔧 refactor: Remove validateImageRequest from middleware exports and enhance validation logic in validateImageRequest.js
* Removed validateImageRequest from the middleware index.
* Improved error handling and validation checks in validateImageRequest.js, including handling of OpenID tokens, URL length, and malformed URLs.
* Updated tests in validateImages.spec.js to cover new validation scenarios and edge cases.