
SonicWall Capture Labs threat research team became aware of the threat CVE-2026-59800, assessed its impact, and developed mitigation measures. The flaw, also known as the 9Router Tailscale Install Endpoint Unauthenticated OS Command Injection, is a critical vulnerability affecting the 9Router AI request proxy (decolua/9router, distributed on npm as 9router) in all versions up to and including 0.4.39, which NVD expresses as any version before 0.4.44. It lets an unauthenticated attacker execute operating system commands on the server by sending a single HTTP POST to the tunnel installation endpoint, because a JSON field intended to carry a sudo password is written as the first line of standard input to a privileged shell. Classified under CWE-78 (OS Command Injection) and CWE-862 (Missing Authorization), and rated CVSS 9.8 (Critical) by NVD, it was reported by vcth4nh and Ductinn through GitHub Security Advisory GHSA-g6g7-pvmx-m74p. Its EPSS score is 1.34% (68th percentile). The Shadowserver Foundation first observed exploitation in the wild on July 4, 2026. Version 0.4.44 adds the missing route pattern to the authorization middleware and stops piping the installation script through standard input, so administrators should upgrade and stop running the service as root.
9Router is an open-source Node.js proxy that routes AI coding assistant traffic from clients such as Claude Code, Codex, Cursor, and Copilot across more than forty upstream model providers. It is built on the Next.js 16 App Router, ships a self-hosted web dashboard, and is distributed through npm, Docker Hub, and GHCR. Created in January 2026, it has accumulated roughly 25,700 GitHub stars. Because the product brokers credentials for commercial model providers, a compromised instance also exposes the operator's upstream API keys.

Two independent defects combine to produce the vulnerability. The first is a missing authorization check. 9Router protects its dashboard and sensitive API routes with Next.js middleware in src/proxy.js, which delegates to a guard in src/dashboardGuard.js. Next.js only invokes middleware for paths listed in the exported matcher array, and in version 0.4.39 that array covers the dashboard, settings, keys, CLI tools, and MCP routes, but never the /api/tunnel family. The guard even maintains a LOCAL_ONLY_PATHS list restricting routes that spawn child processes to loopback callers, but none of it runs here, because the matcher was never updated when the tunnel routes were added.

The consequence is directly observable. A request to a matched route such as /api/keys returns HTTP 401, but a POST to /api/tunnel/tailscale-install reaches its handler with no credential of any kind. That handler parses the request body and takes sudoPassword from it, falling back to a cached value when the field is absent, and the only validation applied is a test that the value is not empty. When the field is missing entirely the endpoint answers HTTP 400 with "Sudo password is required", which confirms to an unauthenticated caller that the handler ran and tells them which field to populate.

The second defect is the command sink. On Linux the handler calls installTailscale, which delegates to installTailscaleLinux in src/lib/tunnel/tailscale.js. That function first fetches the vendor installation script with curl, and only if the download succeeds does it spawn a privileged shell with spawn("sudo", ["-S", "sh"]). It then writes the attacker-controlled sudoPassword value as the first line of that shell's standard input, and writes the downloaded script body immediately afterwards.

The design assumes that sudo -S will consume that first line as a password, an assumption that holds only while sudo actually prompts. Sudo reads standard input for a password solely when it needs to authenticate, so if the process already runs as root, if the invoking account holds a NOPASSWD entry, or if a recent authentication timestamp is cached, sudo does not prompt at all. It immediately executes sh, which reads the same pipe as its script, and the supposed password becomes the first command rather than a credential.
The fix in version 0.4.44 addresses both defects and adds defense in depth. The middleware matcher gains /api/tunnel/:path*, so the guard now runs; the guard adds the tunnel API to its authenticated path list and the install route to the localhost-only list; and the sink itself is rewritten. The patched installTailscaleLinux rejects any sudoPassword containing a newline, then writes the installer to a temporary file and invokes sudo -S sh <path>. Because the shell now takes its program from a file, nothing placed on standard input is interpreted as a command.

The following conditions must be met for successful exploitation of CVE-2026-59800:
Exploiting CVE-2026-59800 requires no credentials, no session, and no special tooling. The entire attack is a single HTTP POST to /api/tunnel/tailscale-install carrying a JSON body whose sudoPassword value is the command to run. Because the middleware never inspects the route, the request carries no authorization of any kind, and that absence is itself a useful detection signal. Appending ; exit 0 or & exit 0 to the payload terminates the shell before the genuine installation script arrives on the pipe, which both prevents Tailscale from actually installing and returns the HTTP response immediately rather than holding the connection open.

The endpoint replies with HTTP 200 and a text/event-stream body rather than JSON, emitting progress events as the handler works. A successful injection produces the sequence "Downloading install script...", then "Running install script...", then a done event, which is the same sequence a legitimate installation produces. A rejected empty-body probe instead returns HTTP 400 with a JSON content type, and a failed injection against a prompting sudo returns an error event carrying "Wrong sudo password". On a vulnerable instance running as root, the injected command executes with full root privilege, which is sufficient to establish an interactive reverse shell on the target host.
Video Demonstration
| Component | Value | Purpose |
|---|---|---|
| Target Endpoint | POST /api/tunnel/tailscale-install | Route absent from the middleware matcher, so no authorization runs |
| Transport | plaintext HTTP on TCP 20128 | Default listener bound to all interfaces, 20129 in some builds |
| Injection Parameter | sudoPassword in the JSON body | Written as the first line of stdin to the privileged shell |
| Required Headers | Content-Type: application/json only | No cookie, bearer token, or x-9r-cli-token needed |
| Execution Primitive | spawn("sudo", ["-S", "sh"]) | Non-prompting sudo executes sh, which reads the payload as a command |
| Terminator | ; exit 0 or & exit 0 | Ends the shell before install.sh arrives and frees the HTTP response |
| Server Response | HTTP 200 with text/event-stream | Progress events match a legitimate installation attempt |
| Resulting Privilege | root on the target host | Also exposes the upstream model provider API keys the proxy stores |
To ensure SonicWall customers are prepared for any exploitation that may occur due to this vulnerability, the following signature has been released:
| Signature ID | Signature Name |
|---|---|
| IPS: 22401 | 9Router tailscale-install OS Command Injection |
The risks posed by CVE-2026-59800 can be mitigated or eliminated with the following measures:
The vulnerability was reported by vcth4nh and Ductinn, and disclosed through the 9Router project as GitHub Security Advisory GHSA-g6g7-pvmx-m74p, published by the maintainer decolua on May 29, 2026. The fix shipped in 9Router 0.4.44.
Third-party vulnerability database mirrors:
Share This Article

An Article By
An Article By
Security News
Security News