🗝️ fix: Exempt Admin-Trusted Domains from MCP OAuth Validation#12255
Merged
Conversation
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.
Pass allowedDomains from MCPServersRegistry through BasicConnectionOptions, MCPConnectionFactory, and into MCPOAuthHandler method calls so the OAuth layer can exempt admin-trusted domains from SSRF validation.
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.
1 task
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates the MCP OAuth SSRF protection logic to respect the admin-configured allowedDomains allowlist, so that OAuth discovery/token/revocation URLs on trusted/internal hosts aren’t blocked, and threads allowedDomains through the MCP connection/inspection flow.
Changes:
- Add
allowedDomainsto MCP connection option plumbing (registry → manager/factory/inspector → OAuth handler). - Exempt OAuth URLs from SSRF checks when their hostname matches the configured
allowedDomains(via newisHostnameAllowedhelper). - Add/extend Jest coverage for the allowlist exemption and update registry mocks accordingly.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/api/src/mcp/types/index.ts | Adds allowedDomains to BasicConnectionOptions for propagation. |
| packages/api/src/mcp/registry/MCPServerInspector.ts | Passes allowedDomains into connection creation during inspection. |
| packages/api/src/mcp/oauth/handler.ts | Threads allowedDomains through OAuth flows and skips SSRF checks for allowlisted hostnames. |
| packages/api/src/mcp/tests/MCPOAuthSecurity.test.ts | Adds tests validating allowlist-based SSRF exemption behavior. |
| packages/api/src/mcp/tests/MCPOAuthRaceCondition.test.ts | Updates registry mocks to include getAllowedDomains(). |
| packages/api/src/mcp/tests/MCPManager.test.ts | Updates registry mock to include getAllowedDomains(). |
| packages/api/src/mcp/tests/ConnectionsRepository.test.ts | Updates registry mock and expected connection options to include allowedDomains. |
| packages/api/src/mcp/UserConnectionManager.ts | Passes registry allowedDomains into connection creation. |
| packages/api/src/mcp/MCPManager.ts | Includes allowedDomains in the BasicConnectionOptions passed to the factory. |
| packages/api/src/mcp/MCPConnectionFactory.ts | Stores and forwards allowedDomains into OAuth handler calls. |
| packages/api/src/mcp/ConnectionsRepository.ts | Passes registry allowedDomains into connection creation. |
| packages/api/src/auth/domain.ts | Introduces isHostnameAllowed() for hostname-level allowlist matching. |
| packages/api/src/auth/domain.spec.ts | Adds unit tests for isHostnameAllowed(). |
| api/server/controllers/UserController.js | Passes registry allowedDomains into OAuth token revocation calls. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
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.
…ains 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.
Prevents future contributors from assuming a missing parameter during security audits — URLs are pre-validated during initiateOAuthFlow.
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.
1 task
jcbartle
pushed a commit
to jcbartle/LibreChat
that referenced
this pull request
May 11, 2026
…-avila#12255) * fix: exempt allowedDomains from MCP OAuth SSRF checks (danny-avila#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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a context-blind SSRF guard that blocked private/internal OAuth endpoints for MCP servers whose hostnames were explicitly listed in
mcpSettings.allowedDomains. AddsisOAuthUrlAllowed()and threadsallowedDomainsthrough the full MCP connection stack so the OAuth layer can honor admin-configured trust.isOAuthUrlAllowed()topackages/api/src/auth/domain.ts— a synchronous allowlist check that mirrorsisDomainAllowedCorelogic, matching the full OAuth URL againstallowedDomainsentries including protocol and explicit port constraints, so a scoped entry likehttps://auth.internal:8443cannot inadvertently exempt other ports on the same host.isOAuthUrlAllowedcheck to the top ofvalidateOAuthUrl, beforenew URL()hostname extraction, so admin-trusted URLs return after a single URL parse rather than two.validateOAuthUrlJSDoc to document that theisOAuthUrlAllowedcheck short-circuits the SSRF/DNS path and honors protocol/port constraints when specified by the admin.allowedDomains?: string[] | nulltoBasicConnectionOptionsinpackages/api/src/mcp/types/index.ts.allowedDomainsfromMCPServersRegistry.getAllowedDomains()throughConnectionsRepository,MCPManager,UserConnectionManager, andMCPConnectionFactoryinto everyMCPOAuthHandlercall site (initiateOAuthFlow,refreshOAuthTokens,revokeOAuthToken,discoverMetadata).allowedDomainsfromMCPServerInspectorintoMCPConnectionFactory.create()when creating temporary inspection connections.allowedDomainsfrom the registry inUserController.jsand forwards it to bothrevokeOAuthTokencalls inmaybeUninstallOAuthMCP.completeOAuthFlowexplaining the intentional absence ofallowedDomains— all URLs originate fromMCPOAuthFlowMetadataalready validated duringinitiateOAuthFlow.isOAuthUrlAllowedcovering exact match, wildcard subdomain, case insensitivity, protocol/port constraint enforcement, and null/empty list guards indomain.spec.ts.MCPOAuthSecurity.test.tscovering: pre-configured OAuth bypass for private IPs and wildcard internal domains; rejection whenallowedDomainsis empty or non-matching; auto-discovery path with a private server allowed viaallowedDomains; auto-discovery rejection whenallowedDomainsdoesn't match;refreshOAuthTokensbranch 3 (noclientInfo, no pre-configured config) with the bypass active; andrevokeOAuthTokenwith a private revocation endpoint inallowedDomains.ConnectionsRepository.test.ts,MCPManager.test.ts,MCPOAuthRaceCondition.test.ts, andMCPConnectionFactory.test.tsto includegetAllowedDomains.Change Type
Testing
All new behavior is covered by automated tests. Tests exercise real code paths per project conventions —
resolveHostnameSSRF(DNS-dependent) is the only mock, keepingisSSRFTargetlive so private IP literals are genuinely blocked before the bypass is evaluated.To reproduce the fixed behavior manually: configure an MCP server in
librechat.yamlwith a private IP OAuth endpoint (e.g.,authorization_url: http://10.0.0.1/authorize) and add10.0.0.1tomcpSettings.allowedDomains. Prior to this fix, initiating the OAuth flow would throwOAuth authorization_url targets a blocked address; after this fix it proceeds normally.Test Configuration:
cd packages/api && npx jest MCPOAuthSecurityfor the OAuth SSRF and bypass integration tests.cd packages/api && npx jest domain.specfor theisOAuthUrlAllowedunit tests.Checklist