From e6648ac960affdd83994d980ca9df8f1b40bd1aa Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Fri, 21 Aug 2026 19:52:54 -0300 Subject: [PATCH] fix(ssrf): honor local-first provider URL flag in outbound guard (#9123) --- ...arch-provider-local-flag-guard-mismatch.md | 1 + src/shared/network/outboundUrlGuardPolicy.ts | 12 ++++- .../outbound-url-guard-local-flag.test.ts | 49 +++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fixes/9123-search-provider-local-flag-guard-mismatch.md create mode 100644 tests/unit/outbound-url-guard-local-flag.test.ts diff --git a/changelog.d/fixes/9123-search-provider-local-flag-guard-mismatch.md b/changelog.d/fixes/9123-search-provider-local-flag-guard-mismatch.md new file mode 100644 index 0000000000..bc51e12103 --- /dev/null +++ b/changelog.d/fixes/9123-search-provider-local-flag-guard-mismatch.md @@ -0,0 +1 @@ +- fix(ssrf): make `getProviderOutboundGuard()` (used for search-provider connection validation, image generation and remote image fetch) honor the local-first default `OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS` the same way the chat validation guard already does, so a LAN-hosted SearXNG/Brave search provider works with only the LOCAL flag set instead of silently requiring `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` ([#9123](https://github.com/diegosouzapw/OmniRoute/issues/9123)). \ No newline at end of file diff --git a/src/shared/network/outboundUrlGuardPolicy.ts b/src/shared/network/outboundUrlGuardPolicy.ts index cf32a0c612..a991647288 100644 --- a/src/shared/network/outboundUrlGuardPolicy.ts +++ b/src/shared/network/outboundUrlGuardPolicy.ts @@ -56,8 +56,18 @@ export function arePrivateProviderUrlsAllowed() { return false; } +/** + * Guard mode for the provider OUTBOUND path (search-provider connection validation, image + * generation, remote image fetch, model discovery — anything that does not go through the + * chat validation path). Precedence — mirrors `getProviderValidationGuard()` (#9123): + * 1. explicit full opt-in (`arePrivateProviderUrlsAllowed`) → "none" (no checks; power users). + * 2. local-first default (`areLocalProviderUrlsAllowed`) → "block-metadata" (allow LAN, block IMDS). + * 3. otherwise → "public-only" (strict). + */ export function getProviderOutboundGuard(): OutboundUrlGuardMode { - return arePrivateProviderUrlsAllowed() ? "none" : "public-only"; + if (arePrivateProviderUrlsAllowed()) return "none"; + if (areLocalProviderUrlsAllowed()) return "block-metadata"; + return "public-only"; } /** diff --git a/tests/unit/outbound-url-guard-local-flag.test.ts b/tests/unit/outbound-url-guard-local-flag.test.ts new file mode 100644 index 0000000000..f754acd60a --- /dev/null +++ b/tests/unit/outbound-url-guard-local-flag.test.ts @@ -0,0 +1,49 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +const ENV_KEYS = [ + "OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS", + "OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS", + "OUTBOUND_SSRF_GUARD_ENABLED", +]; + +function withEnv(overrides: Record, fn: () => void) { + const saved: Record = {}; + for (const k of ENV_KEYS) saved[k] = process.env[k]; + for (const k of ENV_KEYS) delete process.env[k]; + Object.assign(process.env, overrides); + try { + fn(); + } finally { + for (const k of ENV_KEYS) { + if (saved[k] === undefined) delete process.env[k]; + else process.env[k] = saved[k]; + } + } +} + +test("#9123: setting ONLY OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS=true should relax the guard used for search-provider outbound calls (currently does not)", async () => { + const { areLocalProviderUrlsAllowed, getProviderOutboundGuard } = await import( + "../../src/shared/network/outboundUrlGuardPolicy.ts" + ); + + withEnv({ OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS: "true" }, () => { + assert.equal(areLocalProviderUrlsAllowed(), true); + assert.notEqual( + getProviderOutboundGuard(), + "public-only", + "BUG #9123: OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS should relax getProviderOutboundGuard() " + + "(used for search-provider outbound calls) the same way it already relaxes " + + "getProviderValidationGuard() for regular chat providers — it currently has no effect." + ); + }); +}); + +test("#9123 control: OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS=true DOES relax the same guard", async () => { + const { getProviderOutboundGuard } = await import( + "../../src/shared/network/outboundUrlGuardPolicy.ts" + ); + withEnv({ OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS: "true" }, () => { + assert.equal(getProviderOutboundGuard(), "none"); + }); +}); \ No newline at end of file