mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-15 20:26:33 +01:00
🪪 fix: MCP API Responses and OAuth Validation (#12217)
* 🔒 fix: Validate MCP Configs in Server Responses * 🔒 fix: Enhance OAuth URL Validation in MCPOAuthHandler - Introduced validation for OAuth URLs to ensure they do not target private or internal addresses, enhancing security against SSRF attacks. - Updated the OAuth flow to validate both authorization and token URLs before use, ensuring compliance with security standards. - Refactored redirect URI handling to streamline the OAuth client registration process. - Added comprehensive error handling for invalid URLs, improving robustness in OAuth interactions. * 🔒 feat: Implement Permission Checks for MCP Server Management - Added permission checkers for MCP server usage and creation, enhancing access control. - Updated routes for reinitializing MCP servers and retrieving authentication values to include these permission checks, ensuring only authorized users can access these functionalities. - Refactored existing permission logic to improve clarity and maintainability. * 🔒 fix: Enhance MCP Server Response Validation and Redaction - Updated MCP route tests to use `toMatchObject` for better validation of server response structures, ensuring consistency in expected properties. - Refactored the `redactServerSecrets` function to streamline the removal of sensitive information, ensuring that user-sourced API keys are properly redacted while retaining their source. - Improved OAuth security tests to validate rejection of private URLs across multiple endpoints, enhancing protection against SSRF vulnerabilities. - Added comprehensive tests for the `redactServerSecrets` function to ensure proper handling of various server configurations, reinforcing security measures. * chore: eslint * 🔒 fix: Enhance OAuth Server URL Validation in MCPOAuthHandler - Added validation for discovered authorization server URLs to ensure they meet security standards. - Improved logging to provide clearer insights when an authorization server is found from resource metadata. - Refactored the handling of authorization server URLs to enhance robustness against potential security vulnerabilities. * 🔒 test: Bypass SSRF validation for MCP OAuth Flow tests - Mocked SSRF validation functions to allow tests to use real local HTTP servers, facilitating more accurate testing of the MCP OAuth flow. - Updated test setup to ensure compatibility with the new mocking strategy, enhancing the reliability of the tests. * 🔒 fix: Add Validation for OAuth Metadata Endpoints in MCPOAuthHandler - Implemented checks for the presence and validity of registration and token endpoints in the OAuth metadata, enhancing security by ensuring that these URLs are properly validated before use. - Improved error handling and logging to provide better insights during the OAuth metadata processing, reinforcing the robustness of the OAuth flow. * 🔒 refactor: Simplify MCP Auth Values Endpoint Logic - Removed redundant permission checks for accessing the MCP server resource in the auth-values endpoint, streamlining the request handling process. - Consolidated error handling and response structure for improved clarity and maintainability. - Enhanced logging for better insights during the authentication value checks, reinforcing the robustness of the endpoint. * 🔒 test: Refactor LeaderElection Integration Tests for Improved Cleanup - Moved Redis key cleanup to the beforeEach hook to ensure a clean state before each test. - Enhanced afterEach logic to handle instance resignations and Redis key deletion more robustly, improving test reliability and maintainability.
This commit is contained in:
parent
f32907cd36
commit
fa9e1b228a
10 changed files with 845 additions and 102 deletions
|
|
@ -24,6 +24,7 @@ import {
|
|||
selectRegistrationAuthMethod,
|
||||
inferClientAuthMethod,
|
||||
} from './methods';
|
||||
import { isSSRFTarget, resolveHostnameSSRF } from '~/auth';
|
||||
import { sanitizeUrlForLogging } from '~/mcp/utils';
|
||||
|
||||
/** Type for the OAuth metadata from the SDK */
|
||||
|
|
@ -144,7 +145,9 @@ export class MCPOAuthHandler {
|
|||
resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, {}, fetchFn);
|
||||
|
||||
if (resourceMetadata?.authorization_servers?.length) {
|
||||
authServerUrl = new URL(resourceMetadata.authorization_servers[0]);
|
||||
const discoveredAuthServer = resourceMetadata.authorization_servers[0];
|
||||
await this.validateOAuthUrl(discoveredAuthServer, 'authorization_server');
|
||||
authServerUrl = new URL(discoveredAuthServer);
|
||||
logger.debug(
|
||||
`[MCPOAuth] Found authorization server from resource metadata: ${authServerUrl}`,
|
||||
);
|
||||
|
|
@ -200,6 +203,19 @@ export class MCPOAuthHandler {
|
|||
logger.debug(`[MCPOAuth] OAuth metadata discovered successfully`);
|
||||
const metadata = await OAuthMetadataSchema.parseAsync(rawMetadata);
|
||||
|
||||
const endpointChecks: Promise<void>[] = [];
|
||||
if (metadata.registration_endpoint) {
|
||||
endpointChecks.push(
|
||||
this.validateOAuthUrl(metadata.registration_endpoint, 'registration_endpoint'),
|
||||
);
|
||||
}
|
||||
if (metadata.token_endpoint) {
|
||||
endpointChecks.push(this.validateOAuthUrl(metadata.token_endpoint, 'token_endpoint'));
|
||||
}
|
||||
if (endpointChecks.length > 0) {
|
||||
await Promise.all(endpointChecks);
|
||||
}
|
||||
|
||||
logger.debug(`[MCPOAuth] OAuth metadata parsed successfully`);
|
||||
return {
|
||||
metadata: metadata as unknown as OAuthMetadata,
|
||||
|
|
@ -355,10 +371,14 @@ export class MCPOAuthHandler {
|
|||
logger.debug(`[MCPOAuth] Generated flowId: ${flowId}, state: ${state}`);
|
||||
|
||||
try {
|
||||
// Check if we have pre-configured OAuth settings
|
||||
if (config?.authorization_url && config?.token_url && config?.client_id) {
|
||||
logger.debug(`[MCPOAuth] Using pre-configured OAuth settings for ${serverName}`);
|
||||
|
||||
await Promise.all([
|
||||
this.validateOAuthUrl(config.authorization_url, 'authorization_url'),
|
||||
this.validateOAuthUrl(config.token_url, 'token_url'),
|
||||
]);
|
||||
|
||||
const skipCodeChallengeCheck =
|
||||
config?.skip_code_challenge_check === true ||
|
||||
process.env.MCP_SKIP_CODE_CHALLENGE_CHECK === 'true';
|
||||
|
|
@ -410,10 +430,11 @@ export class MCPOAuthHandler {
|
|||
code_challenge_methods_supported: codeChallengeMethodsSupported,
|
||||
};
|
||||
logger.debug(`[MCPOAuth] metadata for "${serverName}": ${JSON.stringify(metadata)}`);
|
||||
const redirectUri = this.getDefaultRedirectUri(serverName);
|
||||
const clientInfo: OAuthClientInformation = {
|
||||
client_id: config.client_id,
|
||||
client_secret: config.client_secret,
|
||||
redirect_uris: [config.redirect_uri || this.getDefaultRedirectUri(serverName)],
|
||||
redirect_uris: [redirectUri],
|
||||
scope: config.scope,
|
||||
token_endpoint_auth_method: tokenEndpointAuthMethod,
|
||||
};
|
||||
|
|
@ -422,7 +443,7 @@ export class MCPOAuthHandler {
|
|||
const { authorizationUrl, codeVerifier } = await startAuthorization(serverUrl, {
|
||||
metadata: metadata as unknown as SDKOAuthMetadata,
|
||||
clientInformation: clientInfo,
|
||||
redirectUrl: clientInfo.redirect_uris?.[0] || this.getDefaultRedirectUri(serverName),
|
||||
redirectUrl: redirectUri,
|
||||
scope: config.scope,
|
||||
});
|
||||
|
||||
|
|
@ -462,8 +483,7 @@ export class MCPOAuthHandler {
|
|||
`[MCPOAuth] OAuth metadata discovered, auth server URL: ${sanitizeUrlForLogging(authServerUrl)}`,
|
||||
);
|
||||
|
||||
/** Dynamic client registration based on the discovered metadata */
|
||||
const redirectUri = config?.redirect_uri || this.getDefaultRedirectUri(serverName);
|
||||
const redirectUri = this.getDefaultRedirectUri(serverName);
|
||||
logger.debug(`[MCPOAuth] Registering OAuth client with redirect URI: ${redirectUri}`);
|
||||
|
||||
const clientInfo = await this.registerOAuthClient(
|
||||
|
|
@ -672,6 +692,24 @@ export class MCPOAuthHandler {
|
|||
return randomBytes(32).toString('base64url');
|
||||
}
|
||||
|
||||
/** Validates an OAuth URL is not targeting a private/internal address */
|
||||
private static async validateOAuthUrl(url: string, fieldName: string): Promise<void> {
|
||||
let hostname: string;
|
||||
try {
|
||||
hostname = new URL(url).hostname;
|
||||
} catch {
|
||||
throw new Error(`Invalid OAuth ${fieldName}: ${sanitizeUrlForLogging(url)}`);
|
||||
}
|
||||
|
||||
if (isSSRFTarget(hostname)) {
|
||||
throw new Error(`OAuth ${fieldName} targets a blocked address`);
|
||||
}
|
||||
|
||||
if (await resolveHostnameSSRF(hostname)) {
|
||||
throw new Error(`OAuth ${fieldName} resolves to a private IP address`);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly STATE_MAP_TYPE = 'mcp_oauth_state';
|
||||
|
||||
/**
|
||||
|
|
@ -783,10 +821,10 @@ export class MCPOAuthHandler {
|
|||
scope: metadata.clientInfo.scope,
|
||||
});
|
||||
|
||||
/** Use the stored client information and metadata to determine the token URL */
|
||||
let tokenUrl: string;
|
||||
let authMethods: string[] | undefined;
|
||||
if (config?.token_url) {
|
||||
await this.validateOAuthUrl(config.token_url, 'token_url');
|
||||
tokenUrl = config.token_url;
|
||||
authMethods = config.token_endpoint_auth_methods_supported;
|
||||
} else if (!metadata.serverUrl) {
|
||||
|
|
@ -813,6 +851,7 @@ export class MCPOAuthHandler {
|
|||
tokenUrl = oauthMetadata.token_endpoint;
|
||||
authMethods = oauthMetadata.token_endpoint_auth_methods_supported;
|
||||
}
|
||||
await this.validateOAuthUrl(tokenUrl, 'token_url');
|
||||
}
|
||||
|
||||
const body = new URLSearchParams({
|
||||
|
|
@ -886,10 +925,10 @@ export class MCPOAuthHandler {
|
|||
return this.processRefreshResponse(tokens, metadata.serverName, 'stored client info');
|
||||
}
|
||||
|
||||
// Fallback: If we have pre-configured OAuth settings, use them
|
||||
if (config?.token_url && config?.client_id) {
|
||||
logger.debug(`[MCPOAuth] Using pre-configured OAuth settings for token refresh`);
|
||||
|
||||
await this.validateOAuthUrl(config.token_url, 'token_url');
|
||||
const tokenUrl = new URL(config.token_url);
|
||||
|
||||
const body = new URLSearchParams({
|
||||
|
|
@ -987,6 +1026,7 @@ export class MCPOAuthHandler {
|
|||
} else {
|
||||
tokenUrl = new URL(oauthMetadata.token_endpoint);
|
||||
}
|
||||
await this.validateOAuthUrl(tokenUrl.href, 'token_url');
|
||||
|
||||
const body = new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
|
|
@ -1036,7 +1076,9 @@ export class MCPOAuthHandler {
|
|||
},
|
||||
oauthHeaders: Record<string, string> = {},
|
||||
): Promise<void> {
|
||||
// build the revoke URL, falling back to the server URL + /revoke if no revocation endpoint is provided
|
||||
if (metadata.revocationEndpoint != null) {
|
||||
await this.validateOAuthUrl(metadata.revocationEndpoint, 'revocation_endpoint');
|
||||
}
|
||||
const revokeUrl: URL =
|
||||
metadata.revocationEndpoint != null
|
||||
? new URL(metadata.revocationEndpoint)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue