mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-09 17:28:51 +00:00
fix(security): exact-host Anthropic baseUrl check (CodeQL js/incomplete-url-substring-sanitization #674) (#5129)
Some checks failed
Publish Fork Image to GHCR / Build and Push Fork Image (push) Waiting to run
CI / i18n UI Coverage (push) Waiting to run
CI / Build language matrix (push) Waiting to run
CI / Change Classification (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Quality Ratchet (push) Blocked by required conditions
CI / Quality Gates (Extended) (push) Waiting to run
CI / Docs Sync (Strict) (push) Waiting to run
CI / Docs Lint (prose — advisory) (push) Waiting to run
CI / i18n Validation (push) Blocked by required conditions
CI / PR Test Policy (push) Waiting to run
CI / Build (push) Blocked by required conditions
CI / Package Artifact (push) Blocked by required conditions
CI / Electron Package Smoke (push) Blocked by required conditions
CI / Unit Tests (1/8) (push) Blocked by required conditions
CI / Unit Tests (2/8) (push) Blocked by required conditions
CI / Unit Tests (3/8) (push) Blocked by required conditions
CI / Unit Tests (4/8) (push) Blocked by required conditions
CI / E2E Tests (3/9) (push) Blocked by required conditions
CI / Unit Tests (5/8) (push) Blocked by required conditions
CI / Unit Tests (6/8) (push) Blocked by required conditions
CI / Unit Tests (7/8) (push) Blocked by required conditions
CI / Unit Tests (8/8) (push) Blocked by required conditions
CI / Vitest (MCP / autoCombo / UI components) (push) Blocked by required conditions
CI / Node 24 Compatibility Tests (1/4) (push) Blocked by required conditions
CI / Node 24 Compatibility Tests (2/4) (push) Blocked by required conditions
CI / Node 24 Compatibility Tests (3/4) (push) Blocked by required conditions
CI / Node 24 Compatibility Tests (4/4) (push) Blocked by required conditions
CI / Node 26 Compatibility Build (push) Blocked by required conditions
CI / Node 26 Compatibility Tests (1/4) (push) Blocked by required conditions
CI / Node 26 Compatibility Tests (2/4) (push) Blocked by required conditions
CI / Node 26 Compatibility Tests (3/4) (push) Blocked by required conditions
CI / Node 26 Compatibility Tests (4/4) (push) Blocked by required conditions
CI / Coverage Shard (1/8) (push) Blocked by required conditions
CI / Coverage Shard (2/8) (push) Blocked by required conditions
CI / Coverage Shard (3/8) (push) Blocked by required conditions
CI / Coverage Shard (4/8) (push) Blocked by required conditions
CI / Coverage Shard (5/8) (push) Blocked by required conditions
CI / Coverage Shard (6/8) (push) Blocked by required conditions
CI / Coverage Shard (7/8) (push) Blocked by required conditions
CI / Coverage Shard (8/8) (push) Blocked by required conditions
CI / Coverage (push) Blocked by required conditions
CI / SonarQube (push) Blocked by required conditions
CI / PR Coverage Comment (push) Blocked by required conditions
CI / E2E Tests (1/9) (push) Blocked by required conditions
CI / E2E Tests (2/9) (push) Blocked by required conditions
CI / E2E Tests (4/9) (push) Blocked by required conditions
CI / E2E Tests (5/9) (push) Blocked by required conditions
CI / E2E Tests (6/9) (push) Blocked by required conditions
CI / E2E Tests (7/9) (push) Blocked by required conditions
CI / E2E Tests (8/9) (push) Blocked by required conditions
CI / E2E Tests (9/9) (push) Blocked by required conditions
CI / Integration Tests (1/2) (push) Blocked by required conditions
CI / Integration Tests (2/2) (push) Blocked by required conditions
CI / Security Tests (push) Blocked by required conditions
CI / CI Dashboard (push) Blocked by required conditions
Publish to Docker Hub / Resolve Docker release metadata (push) Waiting to run
Publish to Docker Hub / Build Docker (linux/amd64) (push) Blocked by required conditions
Publish to Docker Hub / Build Docker (linux/arm64) (push) Blocked by required conditions
Publish to Docker Hub / Publish multi-arch manifests (push) Blocked by required conditions
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
semgrep / semgrep (push) Waiting to run
Wiki Sync / Sync wiki with docs (push) Waiting to run
opencode-plugin CI / Test (Node 22) (push) Has been cancelled
opencode-plugin CI / Test (Node 24) (push) Has been cancelled
opencode-plugin CI / Build (push) Has been cancelled
Some checks failed
Publish Fork Image to GHCR / Build and Push Fork Image (push) Waiting to run
CI / i18n UI Coverage (push) Waiting to run
CI / Build language matrix (push) Waiting to run
CI / Change Classification (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Quality Ratchet (push) Blocked by required conditions
CI / Quality Gates (Extended) (push) Waiting to run
CI / Docs Sync (Strict) (push) Waiting to run
CI / Docs Lint (prose — advisory) (push) Waiting to run
CI / i18n Validation (push) Blocked by required conditions
CI / PR Test Policy (push) Waiting to run
CI / Build (push) Blocked by required conditions
CI / Package Artifact (push) Blocked by required conditions
CI / Electron Package Smoke (push) Blocked by required conditions
CI / Unit Tests (1/8) (push) Blocked by required conditions
CI / Unit Tests (2/8) (push) Blocked by required conditions
CI / Unit Tests (3/8) (push) Blocked by required conditions
CI / Unit Tests (4/8) (push) Blocked by required conditions
CI / E2E Tests (3/9) (push) Blocked by required conditions
CI / Unit Tests (5/8) (push) Blocked by required conditions
CI / Unit Tests (6/8) (push) Blocked by required conditions
CI / Unit Tests (7/8) (push) Blocked by required conditions
CI / Unit Tests (8/8) (push) Blocked by required conditions
CI / Vitest (MCP / autoCombo / UI components) (push) Blocked by required conditions
CI / Node 24 Compatibility Tests (1/4) (push) Blocked by required conditions
CI / Node 24 Compatibility Tests (2/4) (push) Blocked by required conditions
CI / Node 24 Compatibility Tests (3/4) (push) Blocked by required conditions
CI / Node 24 Compatibility Tests (4/4) (push) Blocked by required conditions
CI / Node 26 Compatibility Build (push) Blocked by required conditions
CI / Node 26 Compatibility Tests (1/4) (push) Blocked by required conditions
CI / Node 26 Compatibility Tests (2/4) (push) Blocked by required conditions
CI / Node 26 Compatibility Tests (3/4) (push) Blocked by required conditions
CI / Node 26 Compatibility Tests (4/4) (push) Blocked by required conditions
CI / Coverage Shard (1/8) (push) Blocked by required conditions
CI / Coverage Shard (2/8) (push) Blocked by required conditions
CI / Coverage Shard (3/8) (push) Blocked by required conditions
CI / Coverage Shard (4/8) (push) Blocked by required conditions
CI / Coverage Shard (5/8) (push) Blocked by required conditions
CI / Coverage Shard (6/8) (push) Blocked by required conditions
CI / Coverage Shard (7/8) (push) Blocked by required conditions
CI / Coverage Shard (8/8) (push) Blocked by required conditions
CI / Coverage (push) Blocked by required conditions
CI / SonarQube (push) Blocked by required conditions
CI / PR Coverage Comment (push) Blocked by required conditions
CI / E2E Tests (1/9) (push) Blocked by required conditions
CI / E2E Tests (2/9) (push) Blocked by required conditions
CI / E2E Tests (4/9) (push) Blocked by required conditions
CI / E2E Tests (5/9) (push) Blocked by required conditions
CI / E2E Tests (6/9) (push) Blocked by required conditions
CI / E2E Tests (7/9) (push) Blocked by required conditions
CI / E2E Tests (8/9) (push) Blocked by required conditions
CI / E2E Tests (9/9) (push) Blocked by required conditions
CI / Integration Tests (1/2) (push) Blocked by required conditions
CI / Integration Tests (2/2) (push) Blocked by required conditions
CI / Security Tests (push) Blocked by required conditions
CI / CI Dashboard (push) Blocked by required conditions
Publish to Docker Hub / Resolve Docker release metadata (push) Waiting to run
Publish to Docker Hub / Build Docker (linux/amd64) (push) Blocked by required conditions
Publish to Docker Hub / Build Docker (linux/arm64) (push) Blocked by required conditions
Publish to Docker Hub / Publish multi-arch manifests (push) Blocked by required conditions
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
semgrep / semgrep (push) Waiting to run
Wiki Sync / Sync wiki with docs (push) Waiting to run
opencode-plugin CI / Test (Node 22) (push) Has been cancelled
opencode-plugin CI / Test (Node 24) (push) Has been cancelled
opencode-plugin CI / Build (push) Has been cancelled
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.
This commit is contained in:
parent
555b21d296
commit
b3207ab010
3 changed files with 78 additions and 2 deletions
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
|||
25
open-sse/utils/anthropicHost.ts
Normal file
25
open-sse/utils/anthropicHost.ts
Normal file
|
|
@ -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";
|
||||
}
|
||||
51
tests/unit/anthropic-official-baseurl-host.test.ts
Normal file
51
tests/unit/anthropic-official-baseurl-host.test.ts
Normal file
|
|
@ -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)"
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue