CVE-2026-55641
9router: Unauthenticated `/v1` proxy access via `Host`-header spoofing → open AI relay + SSRF
9Router is an AI router & token saver. Prior to 0.5.2, 9router determines whether a /v1 LLM proxy request is local by reading the client-controlled Host header, allowing a remote unauthenticated attacker to send Host: localhost and bypass API-key authentication. In the default configuration, this exposes the /v1 proxy to upstream provider calls using stored provider credentials and allows /v1/search with the searxng provider_options.baseUrl parameter to drive server-side requests to internal or cloud-metadata hosts. This issue is fixed in version 0.5.2.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- High
- Integrity
- Low
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
8.2
9router: Unauthenticated `/v1` proxy access via `Host`-header spoofing → open AI relay + SSRF
9Router is an AI router & token saver. Prior to 0.5.2, 9router determines whether a /v1 LLM proxy request is local by reading the client-controlled Host header, allowing a remote unauthenticated attacker to send Host: localhost and bypass API-key authentication. In the default configuration, this exposes the /v1 proxy to upstream provider calls using stored provider credentials and allows /v1/search with the searxng provider_options.baseUrl parameter to drive server-side requests to internal or cloud-metadata hosts. This issue is fixed in version 0.5.2.
- Record
CVE-2026-55641- CVSS 3.1
- 8.2 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N - Weakness
- CWE-290, CWE-348, CWE-918, CWE-1327
- Updated
- 2026-11-02
EUVDEUVD-2026-42930
8.2
9Router is an AI router & token saver. Prior to 0.5.2, 9router determines whether a /v1 LLM proxy request is local by reading the client-controlled Host header, allowing a remote unauthenticated attacker to send Host: localhost and bypass API-key authentication. In the default configuration, this exposes the /v1 proxy to upstream provider calls using stored provider credentials and allows /v1/search with the searxng provider_options.baseUrl parameter to drive server-side requests to internal or cloud-metadata hosts. This issue is fixed in version 0.5.2.
- Record
EUVD-2026-42930- CVSS 3.1
- 8.2 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-86m2-fcxq-5q7c
8.2
9router: Unauthenticated `/v1` proxy access via `Host`-header spoofing → open AI relay + SSRF
Summary
9router's request guard decides a request is "local" (and therefore exempt from API-key auth on the /v1 LLM proxy) by reading the client-controlled Host header. Because 9router binds 0.0.0.0 by default (and the CLI misleadingly prints "localhost"), a remote, unauthenticated attacker who can reach the port can send Host: localhost to be treated as local and obtain /v1 proxy access with no API key, no CLI token, and no dashboard login. In the default configuration (requireApiKey is absent from DEFAULT_SETTINGS, so the handler-side key check is skipped), this yields:
-
Open AI relay — the proxy forwards the attacker's requests to AI providers using the victim's stored paid API keys (cost/quota theft, prompt-based data exfiltration through the victim's accounts).
-
Unauthenticated SSRF —
/v1/searchwith the built-innoAuthsearxngprovider takes its outbound fetch URL from the request body (provider_options.baseUrl), so the attacker drives a server-side fetch to any internal/cloud-metadata host and gets the JSON response reflected back. -
Affected:
9router <= 0.4.80(current),src/dashboardGuard.js(isLocalRequest),src/sse/handlers/{chat,search}.js,src/lib/db/repos/settingsRepo.js,cli/cli.js. -
Distinct from the existing advisories GHSA-fhh6-4qxv-rpqj (MCP-plugin RCE, patched) and GHSA-xrrh-p7f2-27vm (legacy
<0.3.75authz bypass).
Details
The bypass (src/dashboardGuard.js)
function isLoopbackHostname(h){ const name=h.split(":")[0].replace(/^\[|\]$/g,"").toLowerCase();
return new Set(["localhost","127.0.0.1","::1"]).has(name); }
function isLocalRequest(request){
if (!isLoopbackHostname(request.headers.get("host"))) return false; // <-- client-controlled Host
const origin = request.headers.get("origin");
if (origin){ try { if (!isLoopbackHostname(new URL(origin).hostname)) return false; } catch { return false; } }
return true;
}
async function canAccessPublicLlmApi(request){
if (isLocalRequest(request)) return true; // <-- "local" => no key required
if (await hasValidCliToken(request)) return true;
return await hasValidApiKey(request);
}
isLocalRequest never consults the socket peer address — only the spoofable Host header (and an absent/loopback Origin). The /v1,/v1beta,/api/v1,/api/v1beta prefixes are gated solely by canAccessPublicLlmApi.
Default exposure
cli/cli.js:63const DEFAULT_HOST = "0.0.0.0";andDockerfileENV HOSTNAME=0.0.0.0/EXPOSE 20128→ reachable from the network by default.cli/cli.js:500,541display"localhost"even when bound to0.0.0.0— operators believe it's local-only.src/lib/db/repos/settingsRepo.jsDEFAULT_SETTINGShas norequireApiKey→ the handler key checks (chat.jsif (settings.requireApiKey),search.jssame) are skipped by default.
Relay chain (verbatim trace, 0.4.71)
middleware (src/proxy.js, matcher covers all paths) → canAccessPublicLlmApi true via spoofed Host → next.config.mjs rewrites /v1/:path*→/api/v1/:path* → src/app/api/v1/messages/route.js POST → handleChat (no independent auth) → only gate falsy requireApiKey → getProviderCredentials() loads the victim's stored credentials → handleChatCore outbound fetch → response returned. No downstream key gate.
SSRF chain
search.js (only gate falsy requireApiKey) → searxng noAuth:true ⇒ handleSearchCore({credentials:null}) → coreBody.provider_options = body.provider_options → callers.js:
export function resolveBaseUrl(config, params){
const override = getProviderSetting(params, "baseUrl"); // reads params.providerOptions.baseUrl FIRST
return (override || config.baseUrl).replace(/\/+$/, "");
}
→ buildSearxngRequest appends /search?q=...&format=json&categories=general → fetch(url) (server-side) → JSON reflected to caller.
PoC
Ground-truth, no network…
- Record
GHSA-86m2-fcxq-5q7c- CVSS 3.1
- 8.2 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N - Weakness
- CWE-290, CWE-348, CWE-918, CWE-1327
- Updated
- 2026-11-02
NVD
8.2
9Router is an AI router & token saver. Prior to 0.5.2, 9router determines whether a /v1 LLM proxy request is local by reading the client-controlled Host header, allowing a remote unauthenticated attacker to send Host: localhost and bypass API-key authentication. In the default configuration, this exposes the /v1 proxy to upstream provider calls using stored provider credentials and allows /v1/search with the searxng provider_options.baseUrl parameter to drive server-side requests to internal or cloud-metadata hosts. This issue is fixed in version 0.5.2.
- Record
CVE-2026-55641- CVSS 3.1
- 8.2 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N - Weakness
- CWE-290, CWE-348, CWE-918, CWE-1327
- Updated
- 2026-11-02
OSV
8.2
9router: Unauthenticated `/v1` proxy access via `Host`-header spoofing → open AI relay + SSRF
9Router is an AI router & token saver. Prior to 0.5.2, 9router determines whether a /v1 LLM proxy request is local by reading the client-controlled Host header, allowing a remote unauthenticated attacker to send Host: localhost and bypass API-key authentication. In the default configuration, this exposes the /v1 proxy to upstream provider calls using stored provider credentials and allows /v1/search with the searxng provider_options.baseUrl parameter to drive server-side requests to internal or cloud-metadata hosts. This issue is fixed in version 0.5.2.
- Record
CVE-2026-55641- CVSS 3.1
- 8.2 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N - Weakness
- CWE-1327, CWE-290, CWE-348, CWE-918
- Updated
- 2026-11-02
OSVGHSA-86m2-fcxq-5q7c
8.2
9router: Unauthenticated `/v1` proxy access via `Host`-header spoofing → open AI relay + SSRF
Summary
9router's request guard decides a request is "local" (and therefore exempt from API-key auth on the /v1 LLM proxy) by reading the client-controlled Host header. Because 9router binds 0.0.0.0 by default (and the CLI misleadingly prints "localhost"), a remote, unauthenticated attacker who can reach the port can send Host: localhost to be treated as local and obtain /v1 proxy access with no API key, no CLI token, and no dashboard login. In the default configuration (requireApiKey is absent from DEFAULT_SETTINGS, so the handler-side key check is skipped), this yields:
-
Open AI relay — the proxy forwards the attacker's requests to AI providers using the victim's stored paid API keys (cost/quota theft, prompt-based data exfiltration through the victim's accounts).
-
Unauthenticated SSRF —
/v1/searchwith the built-innoAuthsearxngprovider takes its outbound fetch URL from the request body (provider_options.baseUrl), so the attacker drives a server-side fetch to any internal/cloud-metadata host and gets the JSON response reflected back. -
Affected:
9router <= 0.4.80(current),src/dashboardGuard.js(isLocalRequest),src/sse/handlers/{chat,search}.js,src/lib/db/repos/settingsRepo.js,cli/cli.js. -
Distinct from the existing advisories GHSA-fhh6-4qxv-rpqj (MCP-plugin RCE, patched) and GHSA-xrrh-p7f2-27vm (legacy
<0.3.75authz bypass).
Details
The bypass (src/dashboardGuard.js)
function isLoopbackHostname(h){ const name=h.split(":")[0].replace(/^\[|\]$/g,"").toLowerCase();
return new Set(["localhost","127.0.0.1","::1"]).has(name); }
function isLocalRequest(request){
if (!isLoopbackHostname(request.headers.get("host"))) return false; // <-- client-controlled Host
const origin = request.headers.get("origin");
if (origin){ try { if (!isLoopbackHostname(new URL(origin).hostname)) return false; } catch { return false; } }
return true;
}
async function canAccessPublicLlmApi(request){
if (isLocalRequest(request)) return true; // <-- "local" => no key required
if (await hasValidCliToken(request)) return true;
return await hasValidApiKey(request);
}
isLocalRequest never consults the socket peer address — only the spoofable Host header (and an absent/loopback Origin). The /v1,/v1beta,/api/v1,/api/v1beta prefixes are gated solely by canAccessPublicLlmApi.
Default exposure
cli/cli.js:63const DEFAULT_HOST = "0.0.0.0";andDockerfileENV HOSTNAME=0.0.0.0/EXPOSE 20128→ reachable from the network by default.cli/cli.js:500,541display"localhost"even when bound to0.0.0.0— operators believe it's local-only.src/lib/db/repos/settingsRepo.jsDEFAULT_SETTINGShas norequireApiKey→ the handler key checks (chat.jsif (settings.requireApiKey),search.jssame) are skipped by default.
Relay chain (verbatim trace, 0.4.71)
middleware (src/proxy.js, matcher covers all paths) → canAccessPublicLlmApi true via spoofed Host → next.config.mjs rewrites /v1/:path*→/api/v1/:path* → src/app/api/v1/messages/route.js POST → handleChat (no independent auth) → only gate falsy requireApiKey → getProviderCredentials() loads the victim's stored credentials → handleChatCore outbound fetch → response returned. No downstream key gate.
SSRF chain
search.js (only gate falsy requireApiKey) → searxng noAuth:true ⇒ handleSearchCore({credentials:null}) → coreBody.provider_options = body.provider_options → callers.js:
export function resolveBaseUrl(config, params){
const override = getProviderSetting(params, "baseUrl"); // reads params.providerOptions.baseUrl FIRST
return (override || config.baseUrl).replace(/\/+$/, "");
}
→ buildSearxngRequest appends /search?q=...&format=json&categories=general → fetch(url) (server-side) → JSON reflected to caller.
PoC
Ground-truth, no network…
- Record
GHSA-86m2-fcxq-5q7c- CVSS 3.1
- 8.2 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N - Weakness
- CWE-1327, CWE-290, CWE-348, CWE-918
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
9routernpm |
before 0.5.2 | 0.5.2 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-55641 is a high vulnerability (CVSS 8.2) in Decolua 9router, published July 10, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.33%, the 25th percentile. Fixed in 9router 0.5.2.
References
github.com/decolua/9router/security/advisories/GHSA-86m2-fcxq-5q7c
github.com/decolua/9router/commit/b282f0554972ea35281520738759d76abcd0b0b3
github.com/decolua/9router/releases/tag/v0.5.2
nvd.nist.gov/vuln/detail/CVE-2026-55641
github.com/advisories/GHSA-86m2-fcxq-5q7c
github.com/CVEProject/cvelistV5/tree/main/cves/2026/55xxx/CVE-2026-55641.json
github.com/decolua/9router
Left out here: the rest of 2 source descriptions. The JSON record has them.