From ba074f566d7df0483ada2daa21b920c3be4a7ee0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 12:44:50 +0000 Subject: [PATCH] fix(security): validate base_url / console_gateway_url as real http(s) URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The config file accepted any value that merely starts with "http" (so even "httpfoo://evil" passed) for base_url and console_gateway_url — origins the client sends the Bearer token to. Validate them with `new URL()` and an http:/https: protocol check instead, rejecting malformed values. Valid http(s) URLs (including custom proxies and local http) are unaffected. https://claude.ai/code/session_017ZGQCjwNQF5Pz96gLUnnG1 --- packages/core/src/config/schema.ts | 20 +++++++++++++++++--- packages/core/tests/index.test.ts | 14 ++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/core/src/config/schema.ts b/packages/core/src/config/schema.ts index c568a2e..7fad89f 100644 --- a/packages/core/src/config/schema.ts +++ b/packages/core/src/config/schema.ts @@ -38,6 +38,21 @@ export interface ConfigFile { const VALID_REGIONS = new Set(["cn", "us", "intl"]); const VALID_OUTPUTS = new Set(["text", "json"]); +/** + * A syntactically valid absolute http(s) URL. Used to validate `base_url` and + * `console_gateway_url` from the config file: the credential-bearing client + * sends the Bearer token to these origins, so a bare `startsWith("http")` check + * (which also accepts e.g. "httpfoo://…") is too loose. + */ +function isHttpUrl(value: string): boolean { + try { + const u = new URL(value); + return u.protocol === "http:" || u.protocol === "https:"; + } catch { + return false; + } +} + export function parseConfigFile(raw: unknown): ConfigFile { if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {}; const obj = raw as Record; @@ -50,8 +65,7 @@ export function parseConfigFile(raw: unknown): ConfigFile { out.access_token = obj.accessToken; if (typeof obj.region === "string" && VALID_REGIONS.has(obj.region)) out.region = obj.region as Region; - if (typeof obj.base_url === "string" && obj.base_url.startsWith("http")) - out.base_url = obj.base_url; + if (typeof obj.base_url === "string" && isHttpUrl(obj.base_url)) out.base_url = obj.base_url; if (typeof obj.output === "string" && VALID_OUTPUTS.has(obj.output)) out.output = obj.output as ConfigFile["output"]; if (typeof obj.output_dir === "string" && obj.output_dir.length > 0) @@ -73,7 +87,7 @@ export function parseConfigFile(raw: unknown): ConfigFile { out.access_key_secret = obj.access_key_secret; if (typeof obj.workspace_id === "string" && obj.workspace_id.length > 0) out.workspace_id = obj.workspace_id; - if (typeof obj.console_gateway_url === "string" && obj.console_gateway_url.startsWith("http")) + if (typeof obj.console_gateway_url === "string" && isHttpUrl(obj.console_gateway_url)) out.console_gateway_url = obj.console_gateway_url; if (typeof obj.telemetry === "boolean") out.telemetry = obj.telemetry; diff --git a/packages/core/tests/index.test.ts b/packages/core/tests/index.test.ts index a81d1d0..613a529 100644 --- a/packages/core/tests/index.test.ts +++ b/packages/core/tests/index.test.ts @@ -1,6 +1,7 @@ import { expect, test } from "vite-plus/test"; import type { Config } from "../src/index.ts"; import { BailianError, ExitCode, McpClient, mapApiError, request } from "../src/index.ts"; +import { parseConfigFile } from "../src/config/schema.ts"; function testConfig(overrides: Partial = {}): Config { return { @@ -173,3 +174,16 @@ test("McpClient uses injected client identity for initialize and User-Agent", as params: { clientInfo: { name: "test-client", version: "9.8.7" } }, }); }); + +test("parseConfigFile accepts only well-formed http(s) base_url / console_gateway_url", () => { + expect(parseConfigFile({ base_url: "https://dashscope.aliyuncs.com" }).base_url).toBe( + "https://dashscope.aliyuncs.com", + ); + expect(parseConfigFile({ base_url: "http://localhost:8080" }).base_url).toBe( + "http://localhost:8080", + ); + // Previously accepted because the value merely "starts with http". + expect(parseConfigFile({ base_url: "httpfoo://evil" }).base_url).toBeUndefined(); + expect(parseConfigFile({ base_url: "not a url" }).base_url).toBeUndefined(); + expect(parseConfigFile({ console_gateway_url: "ftp://x" }).console_gateway_url).toBeUndefined(); +});