From b3207ab010adc4dcb36b72ebd9497b238712566d Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Fri, 26 Jun 2026 23:14:27 -0300 Subject: [PATCH] fix(security): exact-host Anthropic baseUrl check (CodeQL js/incomplete-url-substring-sanitization #674) (#5129) The anthropic-compatible Bearer-fallback gate decided whether a configured baseUrl targeted the official api.anthropic.com host via a substring `.includes("api.anthropic.com")`. A look-alike upstream such as `https://api.anthropic.com.evil.test` or `https://evil.test/?x=api.anthropic.com` matched the substring and was wrongly treated as official, suppressing the Bearer fallback meant for third-party gateways (CodeQL #674, js/incomplete-url-substring-sanitization, high). Replace the substring test with an exported `isOfficialAnthropicBaseUrl()` helper that parses the URL and compares the hostname for exact equality. Empty baseUrl stays official; scheme-less hosts are parsed with an assumed https://; an unparseable baseUrl falls back to third-party (Bearer emitted) as the safer default. Behavior for legitimate official/third-party baseUrls is unchanged. Adds tests/unit/anthropic-official-baseurl-host.test.ts covering official, look-alike, scheme-less, and unparseable inputs plus a static guard that the substring pattern is gone. --- open-sse/executors/default.ts | 4 +- open-sse/utils/anthropicHost.ts | 25 +++++++++ .../anthropic-official-baseurl-host.test.ts | 51 +++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 open-sse/utils/anthropicHost.ts create mode 100644 tests/unit/anthropic-official-baseurl-host.test.ts diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index b6f58a3ccc..8b76b4c486 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -15,6 +15,7 @@ import { import { getGigachatAccessToken } from "../services/gigachatAuth.ts"; import { getRegistryEntry } from "../config/providerRegistry.ts"; import { mergeClientAnthropicBeta } from "../config/anthropicHeaders.ts"; +import { isOfficialAnthropicBaseUrl } from "../utils/anthropicHost.ts"; import { applyProviderRequestDefaults } from "../services/providerRequestDefaults.ts"; import { stripUnsupportedParams } from "../translator/paramSupport.ts"; import { @@ -485,8 +486,7 @@ export class DefaultExecutor extends BaseExecutor { // x-api-key-only behavior to avoid regressing the official path. if (effectiveKey && !headers["Authorization"]) { const baseUrl = credentials?.providerSpecificData?.baseUrl || ""; - const isOfficialAnthropic = - baseUrl === "" || baseUrl.includes("api.anthropic.com"); + const isOfficialAnthropic = isOfficialAnthropicBaseUrl(baseUrl); if (!isOfficialAnthropic) { headers["Authorization"] = `Bearer ${effectiveKey}`; } diff --git a/open-sse/utils/anthropicHost.ts b/open-sse/utils/anthropicHost.ts new file mode 100644 index 0000000000..47f3042282 --- /dev/null +++ b/open-sse/utils/anthropicHost.ts @@ -0,0 +1,25 @@ +/** + * Whether a configured Anthropic `baseUrl` targets the official `api.anthropic.com` host. + * + * Uses exact hostname equality (parsed via `new URL`) instead of a substring `.includes`, + * so a look-alike upstream such as `https://api.anthropic.com.evil.test` or + * `https://evil.test/?x=api.anthropic.com` is correctly treated as third-party + * (CodeQL `js/incomplete-url-substring-sanitization`). An empty baseUrl means the default + * official endpoint; a scheme-less host (e.g. `api.anthropic.com/v1`) is parsed with an + * assumed `https://`; an unparseable baseUrl is treated as third-party (safer default — + * the Bearer fallback is then emitted). + */ +export function isOfficialAnthropicBaseUrl(baseUrl: string): boolean { + if (!baseUrl) return true; + let host: string | null = null; + try { + host = new URL(baseUrl).hostname; + } catch { + try { + host = new URL(`https://${baseUrl}`).hostname; + } catch { + return false; + } + } + return host === "api.anthropic.com"; +} diff --git a/tests/unit/anthropic-official-baseurl-host.test.ts b/tests/unit/anthropic-official-baseurl-host.test.ts new file mode 100644 index 0000000000..fe0cd3a78a --- /dev/null +++ b/tests/unit/anthropic-official-baseurl-host.test.ts @@ -0,0 +1,51 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { isOfficialAnthropicBaseUrl } from "../../open-sse/utils/anthropicHost.ts"; + +// CodeQL #674 (js/incomplete-url-substring-sanitization): the official-Anthropic check +// must use exact hostname equality, not a substring `.includes("api.anthropic.com")`, so a +// look-alike upstream cannot impersonate the official endpoint and suppress the Bearer +// fallback intended for third-party gateways. + +test("official endpoints are recognized (empty / scheme / scheme-less / path)", () => { + assert.equal(isOfficialAnthropicBaseUrl(""), true, "empty baseUrl = default official"); + assert.equal(isOfficialAnthropicBaseUrl("https://api.anthropic.com"), true); + assert.equal(isOfficialAnthropicBaseUrl("https://api.anthropic.com/v1"), true); + assert.equal(isOfficialAnthropicBaseUrl("https://api.anthropic.com/"), true); + // scheme-less host (operator may omit the protocol) is parsed with an assumed https:// + assert.equal(isOfficialAnthropicBaseUrl("api.anthropic.com"), true); + assert.equal(isOfficialAnthropicBaseUrl("api.anthropic.com/v1"), true); +}); + +test("look-alike / third-party hosts are NOT treated as official", () => { + // The exact strings the old substring check would have wrongly accepted: + assert.equal(isOfficialAnthropicBaseUrl("https://api.anthropic.com.evil.test"), false); + assert.equal(isOfficialAnthropicBaseUrl("https://api.anthropic.com.evil.test/v1"), false); + assert.equal(isOfficialAnthropicBaseUrl("https://evil.test/?x=api.anthropic.com"), false); + assert.equal(isOfficialAnthropicBaseUrl("https://evil.test/api.anthropic.com"), false); + assert.equal(isOfficialAnthropicBaseUrl("https://my-gateway.test/anthropic"), false); + // a genuinely different host + assert.equal(isOfficialAnthropicBaseUrl("https://openrouter.ai/api"), false); +}); + +test("unparseable baseUrl falls back to third-party (Bearer emitted)", () => { + assert.equal(isOfficialAnthropicBaseUrl("http://"), false); + assert.equal(isOfficialAnthropicBaseUrl(":::"), false); +}); + +test("source no longer uses substring .includes for the official-host check", () => { + const here = path.dirname(fileURLToPath(import.meta.url)); + const src = fs.readFileSync( + path.join(here, "../../open-sse/executors/default.ts"), + "utf8" + ); + assert.equal( + src.includes('.includes("api.anthropic.com")'), + false, + "default.ts must not substring-match the official Anthropic host (CodeQL #674)" + ); +});