diff --git a/packages/core/src/agents/auth-provider/credential-leak-prevention.test.ts b/packages/core/src/agents/auth-provider/credential-leak-prevention.test.ts new file mode 100644 index 0000000000..b2e349ddf5 --- /dev/null +++ b/packages/core/src/agents/auth-provider/credential-leak-prevention.test.ts @@ -0,0 +1,74 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; +import { GoogleCredentialsAuthProvider } from './google-credentials-provider.js'; +import type { GoogleCredentialsAuthConfig } from './types.js'; +import { GoogleAuth } from 'google-auth-library'; + +vi.mock('google-auth-library', () => ({ + GoogleAuth: vi.fn(), +})); + +describe('Credential Leak Prevention (RCA / PoC Verification)', () => { + const mockConfig: GoogleCredentialsAuthConfig = { + type: 'google-credentials', + }; + + beforeEach(() => { + vi.clearAllMocks(); + (GoogleAuth as unknown as Mock).mockImplementation(() => ({ + getClient: vi.fn().mockResolvedValue({ + getAccessToken: vi.fn().mockResolvedValue({ token: 'leaked-token' }), + credentials: { expiry_date: Date.now() + 3600 * 1000 }, + }), + getIdTokenClient: vi.fn().mockResolvedValue({ + idTokenProvider: { + fetchIdToken: vi.fn().mockResolvedValue('leaked-id-token'), + }, + }), + })); + }); + + it('should FAIL (throw error) when trying to initialize with an untrusted arbitrary remote agent URL (reproducing vulnerability prevention)', () => { + // This test simulates the reproduction scenario: registering a remote agent with an arbitrary external URL + // e.g., http://127.0.0.1:1337 or https://malicious-agent.evil.com + const untrustedUrls = [ + { + url: 'http://127.0.0.1:1337/.well-known/agent.json', + error: /requires HTTPS/, + }, + { + url: 'https://malicious-agent.evil.com/card', + error: /is not an allowed host/, + }, + { + url: 'https://untrusted-third-party.com/agent', + error: /is not an allowed host/, + }, + ]; + + for (const item of untrustedUrls) { + expect(() => { + new GoogleCredentialsAuthProvider(mockConfig, item.url); + }).toThrow(item.error); + } + }); + + it('should SUCCEED only for allowed Google Services (proving the allowlist constraint)', () => { + const trustedUrls = [ + 'https://language.googleapis.com/v1/models', + 'https://vertex-ai-agent.googleapis.com/agent', + 'https://my-secure-service-abc.run.app/card', + ]; + + for (const url of trustedUrls) { + expect(() => { + new GoogleCredentialsAuthProvider(mockConfig, url); + }).not.toThrow(); + } + }); +}); diff --git a/packages/core/src/agents/auth-provider/google-credentials-provider.test.ts b/packages/core/src/agents/auth-provider/google-credentials-provider.test.ts index f9d6ab18b7..3a9896bc43 100644 --- a/packages/core/src/agents/auth-provider/google-credentials-provider.test.ts +++ b/packages/core/src/agents/auth-provider/google-credentials-provider.test.ts @@ -82,6 +82,24 @@ describe('GoogleCredentialsAuthProvider', () => { ), ).not.toThrow(); }); + + it('throws if the protocol is not HTTPS', () => { + expect( + () => + new GoogleCredentialsAuthProvider( + mockConfig, + 'http://language.googleapis.com/v1/models', + ), + ).toThrow(/requires HTTPS/); + + expect( + () => + new GoogleCredentialsAuthProvider( + mockConfig, + 'http://my-cloud-run-service.run.app', + ), + ).toThrow(/requires HTTPS/); + }); }); describe('Token Fetching', () => { diff --git a/packages/core/src/agents/auth-provider/google-credentials-provider.ts b/packages/core/src/agents/auth-provider/google-credentials-provider.ts index bdd11aa613..ff9ef8fc3d 100644 --- a/packages/core/src/agents/auth-provider/google-credentials-provider.ts +++ b/packages/core/src/agents/auth-provider/google-credentials-provider.ts @@ -40,7 +40,14 @@ export class GoogleCredentialsAuthProvider extends BaseA2AAuthProvider { ); } - const hostname = new URL(targetUrl).hostname; + const urlObj = new URL(targetUrl); + if (urlObj.protocol !== 'https:') { + throw new Error( + `Protocol "${urlObj.protocol}" is not secure. Google Credential provider requires HTTPS.`, + ); + } + + const hostname = urlObj.hostname; const isRunAppHost = CLOUD_RUN_HOST_REGEX.test(hostname); if (isRunAppHost) {