From f33221e7f8c702a6f26e0931e60d9517743f808d Mon Sep 17 00:00:00 2001 From: tahodev <85001874+tahodev@users.noreply.github.com> Date: Sat, 26 Sep 2026 02:55:11 +0900 Subject: [PATCH] fix(server): keep provider credentials on configured origin --- .../src/services/provider-http-client.test.ts | 38 +++++++++++++++++++ .../src/services/provider-http-client.ts | 5 +++ 2 files changed, 43 insertions(+) diff --git a/packages/server/src/services/provider-http-client.test.ts b/packages/server/src/services/provider-http-client.test.ts index 3ae9735d..93148d13 100644 --- a/packages/server/src/services/provider-http-client.test.ts +++ b/packages/server/src/services/provider-http-client.test.ts @@ -65,6 +65,44 @@ describe("ProviderHttpClient", () => { ); }); + it.each(["https://other.example.com/events", "//other.example.com/events"])( + "rejects cross-origin endpoint %s before sending credentials", + async (endpoint) => { + const fetchImpl = vi.fn(); + const client = new ProviderHttpClient({ + auth: { token: "secret-token", type: "bearer" }, + baseUrl: "https://api.example.com/v1", + fetchImpl: fetchImpl as unknown as typeof fetch, + providerName: "Example", + }); + + await expect(client.get(endpoint)).rejects.toThrow( + "Provider endpoint must use the configured origin" + ); + expect(fetchImpl).not.toHaveBeenCalled(); + } + ); + + it("allows absolute endpoints on the configured origin", async () => { + const fetchImpl = vi.fn().mockResolvedValue(new Response("{}")); + const client = new ProviderHttpClient({ + auth: { token: "secret-token", type: "bearer" }, + baseUrl: "https://api.example.com/v1", + fetchImpl: fetchImpl as unknown as typeof fetch, + providerName: "Example", + }); + + await client.get("https://api.example.com/v1/events"); + expect(fetchImpl).toHaveBeenCalledWith( + new URL("https://api.example.com/v1/events"), + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: "Bearer secret-token", + }), + }) + ); + }); + it("preserves caller header casing in fetch init", async () => { const fetchImpl = vi .fn() diff --git a/packages/server/src/services/provider-http-client.ts b/packages/server/src/services/provider-http-client.ts index 9e462df2..a10ee305 100644 --- a/packages/server/src/services/provider-http-client.ts +++ b/packages/server/src/services/provider-http-client.ts @@ -232,6 +232,11 @@ export class ProviderHttpClient { if (url.protocol !== "http:" && url.protocol !== "https:") { throw new Error("Provider endpoint must use http or https"); } + // Never send the provider token to an endpoint outside the configured API. + // This also rejects protocol-relative paths such as //other.example/path. + if (url.origin !== new URL(this.#baseUrl).origin) { + throw new Error("Provider endpoint must use the configured origin"); + } for (const key of url.searchParams.keys()) { if (this.#blockedParams.has(key.toLowerCase())) {