From aa5c80dec487ab02513c170ddd59285d94c71f29 Mon Sep 17 00:00:00 2001 From: cornmander Date: Mon, 11 Aug 2025 12:40:30 -0400 Subject: [PATCH] feat(core): add host validation to GoogleCredentialProvider (#5962) Co-authored-by: Brian Ray <62354532+emeryray2002@users.noreply.github.com> --- .../core/src/mcp/google-auth-provider.test.ts | 64 ++++++++++++++++--- packages/core/src/mcp/google-auth-provider.ts | 16 +++++ packages/core/src/tools/mcp-client.test.ts | 6 +- 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/packages/core/src/mcp/google-auth-provider.test.ts b/packages/core/src/mcp/google-auth-provider.test.ts index f481b9e2..65daa74f 100644 --- a/packages/core/src/mcp/google-auth-provider.test.ts +++ b/packages/core/src/mcp/google-auth-provider.test.ts @@ -12,34 +12,78 @@ import { MCPServerConfig } from '../config/config.js'; vi.mock('google-auth-library'); describe('GoogleCredentialProvider', () => { + const validConfig = { + url: 'https://test.googleapis.com', + oauth: { + scopes: ['scope1', 'scope2'], + }, + } as MCPServerConfig; + it('should throw an error if no scopes are provided', () => { - expect(() => new GoogleCredentialProvider()).toThrow( + const config = { + url: 'https://test.googleapis.com', + } as MCPServerConfig; + expect(() => new GoogleCredentialProvider(config)).toThrow( 'Scopes must be provided in the oauth config for Google Credentials provider', ); }); it('should use scopes from the config if provided', () => { + new GoogleCredentialProvider(validConfig); + expect(GoogleAuth).toHaveBeenCalledWith({ + scopes: ['scope1', 'scope2'], + }); + }); + + it('should throw an error for a non-allowlisted host', () => { const config = { + url: 'https://example.com', + oauth: { + scopes: ['scope1', 'scope2'], + }, + } as MCPServerConfig; + expect(() => new GoogleCredentialProvider(config)).toThrow( + 'Host "example.com" is not an allowed host for Google Credential provider.', + ); + }); + + it('should allow luci.app', () => { + const config = { + url: 'https://luci.app', oauth: { scopes: ['scope1', 'scope2'], }, } as MCPServerConfig; new GoogleCredentialProvider(config); - expect(GoogleAuth).toHaveBeenCalledWith({ - scopes: ['scope1', 'scope2'], - }); + }); + + it('should allow sub.luci.app', () => { + const config = { + url: 'https://sub.luci.app', + oauth: { + scopes: ['scope1', 'scope2'], + }, + } as MCPServerConfig; + new GoogleCredentialProvider(config); + }); + + it('should not allow googleapis.com without a subdomain', () => { + const config = { + url: 'https://googleapis.com', + oauth: { + scopes: ['scope1', 'scope2'], + }, + } as MCPServerConfig; + expect(() => new GoogleCredentialProvider(config)).toThrow( + 'Host "googleapis.com" is not an allowed host for Google Credential provider.', + ); }); describe('with provider instance', () => { let provider: GoogleCredentialProvider; beforeEach(() => { - const config = { - oauth: { - scopes: ['scope1', 'scope2'], - }, - } as MCPServerConfig; - provider = new GoogleCredentialProvider(config); + provider = new GoogleCredentialProvider(validConfig); vi.clearAllMocks(); }); diff --git a/packages/core/src/mcp/google-auth-provider.ts b/packages/core/src/mcp/google-auth-provider.ts index 88cd086b..2b52f734 100644 --- a/packages/core/src/mcp/google-auth-provider.ts +++ b/packages/core/src/mcp/google-auth-provider.ts @@ -14,6 +14,8 @@ import { import { GoogleAuth } from 'google-auth-library'; import { MCPServerConfig } from '../config/config.js'; +const ALLOWED_HOSTS = [/^.+\.googleapis\.com$/, /^(.*\.)?luci\.app$/]; + export class GoogleCredentialProvider implements OAuthClientProvider { private readonly auth: GoogleAuth; @@ -29,6 +31,20 @@ export class GoogleCredentialProvider implements OAuthClientProvider { private _clientInformation?: OAuthClientInformationFull; constructor(private readonly config?: MCPServerConfig) { + const url = this.config?.url || this.config?.httpUrl; + if (!url) { + throw new Error( + 'URL must be provided in the config for Google Credentials provider', + ); + } + + const hostname = new URL(url).hostname; + if (!ALLOWED_HOSTS.some((pattern) => pattern.test(hostname))) { + throw new Error( + `Host "${hostname}" is not an allowed host for Google Credential provider.`, + ); + } + const scopes = this.config?.oauth?.scopes; if (!scopes || scopes.length === 0) { throw new Error( diff --git a/packages/core/src/tools/mcp-client.test.ts b/packages/core/src/tools/mcp-client.test.ts index d37c6eae..e54c3e0f 100644 --- a/packages/core/src/tools/mcp-client.test.ts +++ b/packages/core/src/tools/mcp-client.test.ts @@ -568,7 +568,7 @@ describe('mcp-client', () => { const transport = await createTransport( 'test-server', { - httpUrl: 'http://test-server', + httpUrl: 'http://test.googleapis.com', authProviderType: AuthProviderType.GOOGLE_CREDENTIALS, oauth: { scopes: ['scope1'], @@ -587,7 +587,7 @@ describe('mcp-client', () => { const transport = await createTransport( 'test-server', { - url: 'http://test-server', + url: 'http://test.googleapis.com', authProviderType: AuthProviderType.GOOGLE_CREDENTIALS, oauth: { scopes: ['scope1'], @@ -615,7 +615,7 @@ describe('mcp-client', () => { false, ), ).rejects.toThrow( - 'No URL configured for Google Credentials MCP server', + 'URL must be provided in the config for Google Credentials provider', ); }); });