Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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();
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading