CVE-2026-49353: 9router has an Incomplete Fix Local-Only Access Gate Bypass Vulnerability

HIGH
Advisory Identifier: GHSA-6g2f-w7g3-77vf
CVSS 7.5
CWE-290
Published
Advisory Summary

CVE-2026-49353 is a HIGH (CVSS 7.5) local-only access gate bypass vulnerability (CWE-290: Authentication Bypass by Spoofing) affecting 9router has an Incomplete Fix libraries in the npm ecosystem. The issue involves local-only access gate bypass in 9router via host header spoofing. Remediation is available in the latest vendor maintenance releases.

CVE-2026-49353 at a Glance

CVE Identifier CVE-2026-49353
GHSA Identifier GHSA-6g2f-w7g3-77vf
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-290: Authentication Bypass by Spoofing
Affected Product 9router has an Incomplete Fix
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 7.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:H/A:N).

Attack Vector Network (AV:N)
Attack Complexity High (AC:H)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Changed (S:C)
Confidentiality Low (C:L)
Integrity High (I:H)
Availability None (A:N)

How to Fix CVE-2026-49353

To remediate this vulnerability, upgrade all affected 9router has an Incomplete Fix dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

9router fixed: Latest secure release
npm install 9router@Latest secure release
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls 9router

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-49353, inspect your project dependencies using npm:

npm ls 9router

Analyze the terminal output to locate any version of 9router has an Incomplete Fix in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than the patched release), your application is affected.

Note: The npm ls 9router command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-290: Authentication Bypass by Spoofing, where this attack-focused weakness is caused by incorrectly implemented authentication schemes that are subject to spoofing attacks. In 9router has an Incomplete Fix, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary The fix for CVE-2026-46339 (unauthenticated RCE via unprotected MCP plugin routes) introduced a local-only access gate in `src/dashboardGuard.js` that restricts spawn-capable routes (`/api/mcp/*`, `/api/tunnel/*`, `/api/cli-tools/*`) to loopback requests. The gate determines "local" by inspecting the `Host` and `Origin` HTTP headers rather than the TCP source address. When 9router is deployed behind a reverse proxy, tunnel (Cloudflare Tunnel, Tailscale — both natively supported), or is subject to DNS rebinding, these headers are attacker-controlled, allowing the local-only gate to be bypassed. A second factor (CLI token or JWT cookie) is required by `canAccessLocalOnlyRoute()`, but the CLI token is a deterministic HMAC of the machine ID (`getConsistentMachineId`), which is stable and predictable on cloud VMs. If the attacker can obtain or guess the machine ID (e.g., via another information disclosure, or on shared-tenant infrastructure), the full chain to MCP child process stdin injection is reachable. This is a variant / incomplete fix of CVE-2026-46339 — the same attack surface (remote → MCP child process stdin) remains reachable under specific but realistic deployment configurations. ## Root Cause `isLocalRequest()` at `src/dashboardGuard.js:93-101`: ```javascript function isLocalRequest(request) { if (!isLoopbackHostname(request.headers.get("host"))) return false; const origin = request.headers.get("origin"); if (origin) { try { if (!isLoopbackHostname(new URL(origin).hostname)) return false; } catch { return false; } } return true; } ``` This function trusts `Host` and `Origin` headers as proof of local origin. Both are attacker-controlled in any proxied deployment. The `LOOPBACK_HOSTS` set (`localhost`, `127.0.0.1`, `::1`) is checked against these headers, not against the actual connection source IP. ## Attack Scenario ### Scenario 1: Cloudflare Tunnel / Tailscale Funnel 9router natively supports Cloudflare Tunnel and Tailscale (see `LOCAL_ONLY_PATHS` entries for `/api/tunnel/*`). When exposed via tunnel: 1. Attacker sends request to `https://<tunnel-domain>/api/mcp/<plugin>/sse` 2. Sets `Host: localhost:3000` and `Origin: http://localhost:3000` 3. `isLocalRequest()` returns `true` 4. `canAccessLocalOnlyRoute()` then requires CLI token or (local + JWT) 5. CLI token is `getConsistentMachineId("9r-cli-auth")` — a deterministic HMAC of the machine's hardware/OS identifiers ### Scenario 2: DNS Rebinding 1. Attacker controls `evil.com` DNS, initially resolving to attacker IP 2. Victim's browser navigates to `evil.com` (or via iframe/redirect) 3. DNS rebinding switches `evil.com` → `127.0.0.1` 4. Subsequent fetch to `evil.com:3000/api/mcp/<plugin>/message` reaches 9router 5. `Host` header is `evil.com:3000` — this is **blocked** by the current check (not in LOOPBACK_HOSTS) 6. However, if the attacker uses `localhost:3000` as the request host via CORS or service worker tricks, and the browser sends `Host: localhost:3000`, the gate opens ### Exploitation (when CLI token is obtained) Once past the gate, the attacker can: 1. `GET /api/mcp/<plugin>/sse` — establish SSE session, get `sessionId` 2. `POST /api/mcp/<plugin>/message` — send arbitrary JSON-RPC to the child process stdin 3. The child process is one of: `npx`, `node`, `python`, `python3`, `uvx`, `bunx`, `bun` 4. Depending on the MCP plugin implementation, this can achieve arbitrary code execution on the host ## Steps to Reproduce 1. Deploy 9router behind a reverse proxy or tunnel 2. From a remote host, send: ```http GET /api/mcp/browser/sse HTTP/1.1 Host: localhost:3000 Origin: http://localhost:3000 x-9r-cli-token: <machine-id-derived-token> ``` 3. Observe: SSE connection established, `endpoint` event received with message URL 4. POST arbitrary JSON-RPC to the message endpoint ## Impact An attacker who can reach a proxied/tunneled 9router instance and obtain the deterministic CLI token can bypass the local-only restriction and interact with MCP child processes (node, python, npx, etc.) via stdin. This achieves the same impact as CVE-2026-46339: remote code execution on the host. The severity is reduced from CVE-2026-46339's CVSS 10.0 because: - Requires proxied/tunneled deployment (not default localhost-only) - Requires obtaining the CLI token (deterministic but not trivially guessable without another primitive) ## Remediation 1. **Check actual source IP, not headers.** Use `request.ip`, `request.socket.remoteAddress`, or a trusted `X-Forwarded-For` header with known proxy configuration instead of `Host`/`Origin` for the local-only gate. 2. **Make CLI token non-deterministic.** Generate a random token on first run and persist it, rather than deriving from machine ID. Machine IDs are often predictable or discoverable on cloud infrastructure. 3. **Bind MCP routes to loopback at the network layer.** If MCP is local-only by design, the server should bind those routes to `127.0.0.1` only, not rely on middleware header checks. Credit: @snailsploit

CVE-2026-49353 Affected 9router has an Incomplete Fix Versions

1 component(s)
Product / Package Ecosystem Affected Versions Fixed Version
9router
npm
≤ 0.4.55

References

Frequently Asked Questions

What is CVE-2026-49353?
CVE-2026-49353 is a security vulnerability in 9router has an Incomplete Fix involving Local-Only Access Gate Bypass in 9router via Host Header SpoofING under CWE-290: Authentication Bypass by Spoofing.
Which 9router has an Incomplete Fix versions are affected?
Affected versions include releases prior to current release patches across affected packages: 9router.
What version fixes CVE-2026-49353?
Upgrading to current release patches (or the latest patched release for your respective branch) resolves the vulnerability.
Is 9router affected?
Yes, 9router is affected in releases prior to Latest secure release. Upgrade your dependency to Latest secure release or later.
What is the CVSS score of CVE-2026-49353?
CVE-2026-49353 has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:H/A:N.
What CWE is associated with CVE-2026-49353?
CVE-2026-49353 is classified under CWE-290: Authentication Bypass by Spoofing (This attack-focused weakness is caused by incorrectly implemented authentication schemes that are subject to spoofing attacks.).
How can I check npm dependencies for CVE-2026-49353?
Run the dependency tree command: 'npm ls 9router'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-6g2f-w7g3-77vf",
  "modified" : "2026-07-02T21:13:19Z",
  "published" : "2026-07-02T21:13:19Z",
  "aliases" : [ "CVE-2026-49353" ],
  "summary" : "9router has an Incomplete Fix: Local-Only Access Gate Bypass in 9router via Host Header SpoofING",
  "details" : "## Summary\n\nThe fix for CVE-2026-46339 (unauthenticated RCE via unprotected MCP plugin routes) introduced a local-only access gate in `src/dashboardGuard.js` that restricts spawn-capable routes (`/api/mcp/*`, `/api/tunnel/*`, `/api/cli-tools/*`) to loopback requests. The gate determines \"local\" by inspecting the `Host` and `Origin` HTTP headers rather than the TCP source address. When 9router is deployed behind a reverse proxy, tunnel (Cloudflare Tunnel, Tailscale — both natively supported), or is subject to DNS rebinding, these headers are attacker-controlled, allowing the local-only gate to be bypassed.\n\nA second factor (CLI token or JWT cookie) is required by `canAccessLocalOnlyRoute()`, but the CLI token is a deterministic HMAC of the machine ID (`getConsistentMachineId`), which is stable and predictable on cloud VMs. If the attacker can obtain or guess the machine ID (e.g., via another information disclosure, or on shared-tenant infrastructure), the full chain to MCP child process stdin injection is reachable.\n\nThis is a variant / incomplete fix of CVE-2026-46339 — the same attack surface (remote → MCP child process stdin) remains reachable under specific but realistic deployment configurations.\n\n## Root Cause\n\n`isLocalRequest()` at `src/dashboardGuard.js:93-101`:\n\n```javascript\nfunction isLocalRequest(request) {\n  if (!isLoopbackHostname(request.headers.get(\"host\"))) return false;\n  const origin = request.headers.get(\"origin\");\n  if (origin) {\n    try {\n      if (!isLoopbackHostname(new URL(origin).hostname)) return false;\n    } catch { return false; }\n  }\n  return true;\n}\n```\n\nThis function trusts `Host` and `Origin` headers as proof of local origin. Both are attacker-controlled in any proxied deployment. The `LOOPBACK_HOSTS` set (`localhost`, `127.0.0.1`, `::1`) is checked against these headers, not against the actual connection source IP.\n\n## Attack Scenario\n\n### Scenario 1: Cloudflare Tunnel / Tailscale Funnel\n\n9router natively supports Cloudflare Tunnel and Tailscale (see `LOCAL_ONLY_PATHS` entries for `/api/tunnel/*`). When exposed via tunnel:\n\n1. Attacker sends request to `https://<tunnel-domain>/api/mcp/<plugin>/sse`\n2. Sets `Host: localhost:3000` and `Origin: http://localhost:3000`\n3. `isLocalRequest()` returns `true`\n4. `canAccessLocalOnlyRoute()` then requires CLI token or (local + JWT)\n5. CLI token is `getConsistentMachineId(\"9r-cli-auth\")` — a deterministic HMAC of the machine's hardware/OS identifiers\n\n### Scenario 2: DNS Rebinding\n\n1. Attacker controls `evil.com` DNS, initially resolving to attacker IP\n2. Victim's browser navigates to `evil.com` (or via iframe/redirect)\n3. DNS rebinding switches `evil.com` → `127.0.0.1`\n4. Subsequent fetch to `evil.com:3000/api/mcp/<plugin>/message` reaches 9router\n5. `Host` header is `evil.com:3000` — this is **blocked** by the current check (not in LOOPBACK_HOSTS)\n6. However, if the attacker uses `localhost:3000` as the request host via CORS or service worker tricks, and the browser sends `Host: localhost:3000`, the gate opens\n\n### Exploitation (when CLI token is obtained)\n\nOnce past the gate, the attacker can:\n\n1. `GET /api/mcp/<plugin>/sse` — establish SSE session, get `sessionId`\n2. `POST /api/mcp/<plugin>/message` — send arbitrary JSON-RPC to the child process stdin\n3. The child process is one of: `npx`, `node`, `python`, `python3`, `uvx`, `bunx`, `bun`\n4. Depending on the MCP plugin implementation, this can achieve arbitrary code execution on the host\n\n## Steps to Reproduce\n\n1. Deploy 9router behind a reverse proxy or tunnel\n2. From a remote host, send:\n\n```http\nGET /api/mcp/browser/sse HTTP/1.1\nHost: localhost:3000\nOrigin: http://localhost:3000\nx-9r-cli-token: <machine-id-derived-token>\n```\n\n3. Observe: SSE connection established, `endpoint` event received with message URL\n4. POST arbitrary JSON-RPC to the message endpoint\n\n## Impact\n\nAn attacker who can reach a proxied/tunneled 9router instance and obtain the deterministic CLI token can bypass the local-only restriction and interact with MCP child processes (node, python, npx, etc.) via stdin. This achieves the same impact as CVE-2026-46339: remote code execution on the host.\n\nThe severity is reduced from CVE-2026-46339's CVSS 10.0 because:\n- Requires proxied/tunneled deployment (not default localhost-only)\n- Requires obtaining the CLI token (deterministic but not trivially guessable without another primitive)\n\n## Remediation\n\n1. **Check actual source IP, not headers.** Use `request.ip`, `request.socket.remoteAddress`, or a trusted `X-Forwarded-For` header with known proxy configuration instead of `Host`/`Origin` for the local-only gate.\n\n2. **Make CLI token non-deterministic.** Generate a random token on first run and persist it, rather than deriving from machine ID. Machine IDs are often predictable or discoverable on cloud infrastructure.\n\n3. **Bind MCP routes to loopback at the network layer.** If MCP is local-only by design, the server should bind those routes to `127.0.0.1` only, not rely on middleware header checks.\n\n\nCredit: @snailsploit",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:H/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "9router"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "last_affected" : "0.4.55"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/decolua/9router/security/advisories/GHSA-6g2f-w7g3-77vf"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/decolua/9router/commit/5e1c1261368e06dced1cbc650684561b2c8844db"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/decolua/9router/commit/bb86808582067e4fc6f004508a919efb9970d1d5"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/decolua/9router"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-290" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-02T21:13:19Z",
    "nvd_published_at" : null
  }
}