diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index bb3b6e0f9f..94bb036c95 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -10,6 +10,7 @@ export { } from "./providers/registry/alibaba/index.ts"; export { REGISTRY } from "./providers/index.ts"; import { REGISTRY } from "./providers/index.ts"; +import { isPrivateHost } from "@/shared/network/outboundUrlGuard"; import { RegistryModel, REASONING_UNSUPPORTED, @@ -132,11 +133,8 @@ export function isLocalProvider(baseUrl?: string | null): boolean { try { const url = new URL(baseUrl); const hostname = url.hostname; - // Strictly matching 172.16.0.0/12 (Docker/local) and explicitly blocking ::1 per SSRF hardening - return ( - LOCAL_HOSTNAMES.has(hostname) || - /^172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}$/.test(hostname) - ); + if (!hostname) return false; + return LOCAL_HOSTNAMES.has(hostname) || isPrivateHost(hostname); } catch { return false; } diff --git a/tests/unit/is-local-provider-11091.test.ts b/tests/unit/is-local-provider-11091.test.ts new file mode 100644 index 0000000000..e5adc57b15 --- /dev/null +++ b/tests/unit/is-local-provider-11091.test.ts @@ -0,0 +1,38 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { isLocalProvider } from "../../open-sse/config/providerRegistry.ts"; + +test("isLocalProvider detects RFC1918, CGNAT/Tailscale, and mDNS private hosts", () => { + // Local / loopback + assert.equal(isLocalProvider("http://localhost:11434/v1"), true); + assert.equal(isLocalProvider("http://127.0.0.1:11434/v1"), true); + + // Docker 172.16/12 + assert.equal(isLocalProvider("http://172.18.0.2:11434/v1"), true); + + // RFC1918 LAN hosts (Issue #11091) + assert.equal(isLocalProvider("http://192.168.1.50:11434/v1"), true); + assert.equal(isLocalProvider("http://10.0.0.5:11434/v1"), true); + + // Tailscale / CGNAT (100.64/10) + assert.equal(isLocalProvider("http://100.64.1.2:11434/v1"), true); + + // Link-local (169.254/16) + assert.equal(isLocalProvider("http://169.254.1.1:11434/v1"), true); + + // mDNS / private suffixes + assert.equal(isLocalProvider("http://studio.local:11434/v1"), true); + assert.equal(isLocalProvider("http://mybox.internal:11434/v1"), true); + + // Public hosts (should be false) + assert.equal(isLocalProvider("https://api.openai.com/v1"), false); + assert.equal(isLocalProvider("https://api.anthropic.com/v1"), false); + assert.equal(isLocalProvider("http://8.8.8.8:8080/v1"), false); + + // Fails open on missing or unparseable input (Issue #11091 review finding) + assert.equal(isLocalProvider(null), false); + assert.equal(isLocalProvider(undefined), false); + assert.equal(isLocalProvider(""), false); + assert.equal(isLocalProvider("not a url"), false); + assert.equal(isLocalProvider("file:///models"), false); +});