GHSA-fhh6-4qxv-rpqj 9router: Unauthenticated Remote Code Execution via unprotected MCP custom plugin routes
Summary9router 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 Cause1. Middleware Allowlist Is Too Narrow**File:** `src/proxy.js`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–319The `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–1163. Unguarded SSE Endpoint Triggers `spawn()` with Stored Command**File:** `src/app/api/mcp/[plugin]/sse/route.js`, lines 6–25**File:** `src/lib/mcp/stdioSseBridge.js`, line 138`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**Time to exploit from first request:** < 2 seconds.  
**Prerequisites:** Network access to port 20128 (Docker default: `0.0.0.0:20128`).Proof of ConceptPoC 1 — File Write (no listener required)**Observed output (on local test instance):**PoC 2 — Automated PoC scriptThe 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.RemediationFix 1 — Extend middleware matcher (minimal fix)**File:** `src/proxy.js`Fix 2 — Validate `command` in `registerCustomPlugin` (defense-in-depth)**File:** `src/lib/mcp/stdioSseBridge.js`Fix 3 — Sanitize `customPlugins` at the API boundary**File:** `src/app/api/cli-tools/cowork-settings/route.js`, line 312**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.
CVSS v3.1 score: 10