From 4220c810eeb70d8eddf0d2cdfe154db56971995c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rouzbeh=E2=80=A0?= <78313022+rqzbeh@users.noreply.github.com> Date: Sat, 22 Aug 2026 22:38:22 +0330 Subject: [PATCH] fix(resilience): include RFC1918, CGNAT, and mDNS hosts in isLocalProvider (#11091) (#11122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-picked both value commits (a1fa49a8 + a91f6e93, incl. the fail-open guard for unparseable hostnames) onto the current tip, dropping the stale base-red sync commits. Focused tests: is-local-provider-11091 + the isLocalProvider consumer suites (provider-validation-specialty, ollama-local-provider) 127/127; mutation coverage no-drift. Fixes #11091 — RFC1918/CGNAT/link-local/mDNS hosts now classify as local via the existing isPrivateHost. Thank you @rqzbeh! --- open-sse/config/providerRegistry.ts | 8 ++--- tests/unit/is-local-provider-11091.test.ts | 38 ++++++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 tests/unit/is-local-provider-11091.test.ts 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); +});