From d85a47c40c95e975bd7bc5036ce07fe670049eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 26 Jun 2026 19:31:32 -0400 Subject: [PATCH] Harden custom provider endpoint validation --- .../src/main/services/provider-auth-profiles.ts | 12 ++++-------- .../tests/contract/provider-contracts.test.ts | 14 ++++++++++++++ .../tests/unit/provider-service.test.ts | 8 ++++++++ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/AgentCompany-New/src/main/services/provider-auth-profiles.ts b/AgentCompany-New/src/main/services/provider-auth-profiles.ts index 19ffe39a..2f6e0b61 100644 --- a/AgentCompany-New/src/main/services/provider-auth-profiles.ts +++ b/AgentCompany-New/src/main/services/provider-auth-profiles.ts @@ -75,12 +75,12 @@ export function parseVertexProfile(secret: string): ProviderAuthProfile { export function parseOpenAiCompatProfile(secret: string): ProviderAuthProfile { const parsed = parseCloudSecret(secret); - return { type: "openai_compat", baseUrl: normalizedEndpointUrl(requiredString(parsed.baseUrl, "Base URL"), "Base URL"), apiKey: requiredString(parsed.secret, "API key") }; + return { type: "openai_compat", baseUrl: normalizedHttpsUrl(requiredString(parsed.baseUrl, "Base URL"), "Base URL"), apiKey: requiredString(parsed.secret, "API key") }; } export function parseAnthropicCompatProfile(secret: string): ProviderAuthProfile { const parsed = parseCloudSecret(secret); - return { type: "anthropic_compat", baseUrl: normalizedEndpointUrl(requiredString(parsed.baseUrl, "Base URL"), "Base URL"), apiKey: requiredString(parsed.secret, "API key") }; + return { type: "anthropic_compat", baseUrl: normalizedHttpsUrl(requiredString(parsed.baseUrl, "Base URL"), "Base URL"), apiKey: requiredString(parsed.secret, "API key") }; } export function requireOpenAiCompatProfile(profile: ProviderAuthProfile): Extract { @@ -159,11 +159,7 @@ function nonEmptySecret(secret: string, label: string): string { function normalizedHttpsUrl(value: string, label: string): string { const url = new URL(value); if (url.protocol !== "https:") throw new Error(`${label} must use HTTPS.`); - return url.toString().replace(/\/+$/, ""); -} - -function normalizedEndpointUrl(value: string, label: string): string { - const url = new URL(value); - if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error(`${label} must be an http(s) URL.`); + if (url.username || url.password) throw new Error(`${label} must not include credentials.`); + if (url.search || url.hash) throw new Error(`${label} must not include query strings or fragments.`); return url.toString().replace(/\/+$/, ""); } diff --git a/AgentCompany-New/tests/contract/provider-contracts.test.ts b/AgentCompany-New/tests/contract/provider-contracts.test.ts index 782b1086..dc01398d 100644 --- a/AgentCompany-New/tests/contract/provider-contracts.test.ts +++ b/AgentCompany-New/tests/contract/provider-contracts.test.ts @@ -85,6 +85,20 @@ describe("provider contracts", () => { deployment: "gpt-5.4", secret: "azure-key", }))).toThrow("Azure base URL must use HTTPS."); + for (const providerKey of ["custom-openai", "custom-anthropic"]) { + expect(() => providerAdapters[providerKey].parseAuthProfile(JSON.stringify({ + baseUrl: "http://api.example.com/v1", + secret: "sk-custom", + }))).toThrow("Base URL must use HTTPS."); + expect(() => providerAdapters[providerKey].parseAuthProfile(JSON.stringify({ + baseUrl: "https://user:token@api.example.com/v1", + secret: "sk-custom", + }))).toThrow("Base URL must not include credentials."); + expect(() => providerAdapters[providerKey].parseAuthProfile(JSON.stringify({ + baseUrl: "https://api.example.com/v1?tenant=acme", + secret: "sk-custom", + }))).toThrow("Base URL must not include query strings or fragments."); + } }); // Providers with no enumerable models endpoint keep returning the curated diff --git a/AgentCompany-New/tests/unit/provider-service.test.ts b/AgentCompany-New/tests/unit/provider-service.test.ts index 3decdfad..af865da1 100644 --- a/AgentCompany-New/tests/unit/provider-service.test.ts +++ b/AgentCompany-New/tests/unit/provider-service.test.ts @@ -604,10 +604,12 @@ describe("provider service", () => { const providers = new ProviderService(connection.db); let orgId = ""; let azureProviderId = ""; + let customOpenAiProviderId = ""; connection.transaction(() => { orgId = createOrganization(organization); azureProviderId = providerId(connection, orgId, "azure-openai"); + customOpenAiProviderId = providerId(connection, orgId, "custom-openai"); }); expect(() => connection.transaction(() => providers.saveCredential({ @@ -616,6 +618,12 @@ describe("provider service", () => { secret: JSON.stringify({ baseUrl: "http://example.com", deployment: "gpt-5", secret: "key" }), model: "gpt-5", }))).toThrow("Azure base URL must use HTTPS."); + expect(() => connection.transaction(() => providers.saveCredential({ + orgId, + providerId: customOpenAiProviderId, + secret: JSON.stringify({ baseUrl: "http://api.example.com/v1", secret: "key" }), + model: "gpt-5", + }))).toThrow("Base URL must use HTTPS."); connection.close(); }); });