# RCA Report — CVE-2026-59800 / 9router Unauthenticated OS Command Injection

## Summary

9router versions before 0.4.44 expose unauthenticated command execution through `POST /api/tunnel/tailscale-install`. The endpoint is not covered by the vulnerable dashboard authorization middleware matcher, so a remote client can invoke it without credentials. The handler passes the request-body `sudoPassword` field to stdin of `sudo -S sh` before writing the intended Tailscale install script. When sudo does not consume stdin for a password prompt (root process, NOPASSWD sudo, or cached sudo timestamp), `sh` reads the attacker-controlled `sudoPassword` bytes as shell commands. This run demonstrates the command boundary through the real 9router HTTP service and records marker files created by the attacker-supplied command, plus a fixed-version negative control.

## Impact

- **Package / component:** npm package `9router`; Next.js standalone server route `/api/tunnel/tailscale-install`; Linux Tailscale install helper.
- **Affected versions:** `< 0.4.44`; reproduced against `9router@0.4.39`.
- **Fixed version used for negative control:** `9router@0.4.45` (patched behavior: unauthenticated request receives HTTP 401 and the install helper no longer feeds password bytes as shell script input).
- **Risk level and consequences:** Critical. Under the documented no-prompt sudo precondition, an unauthenticated HTTP request can execute attacker-selected shell commands as the 9router process user. In root/container deployments this is root command execution.

## Impact Parity

- **Disclosed / claimed maximum impact:** unauthenticated remote OS command execution (`code_execution`).
- **Reproduced impact from this run:** attacker-controlled commands were executed through the real unauthenticated `POST /api/tunnel/tailscale-install` endpoint on the vulnerable build. The reproduction script ran the actual 9router npm package and actual Next.js server, passed `/api/health`, sent JSON containing `sudoPassword`, reached the vulnerable `spawn("sudo", ["-S", "sh"])` path, and wrote marker artifacts containing `PWNED_BY_9ROUTER_*` plus `id` output. Two vulnerable attempts succeeded. Two fixed attempts returned HTTP 401 and produced no marker.
- **Parity:** full for the demonstrated command-execution boundary under the no-prompt sudo precondition. The current sandbox has no Docker daemon and no real root/NOPASSWD sudo, so the script used a PATH-local no-prompt sudo precondition shim for the successful marker run; if Docker or real root/NOPASSWD is available, the same script first attempts those stronger production-like modes.
- **Not demonstrated:** actual `uid=0` output in this sandbox, because the sandbox user is unprivileged (`vscode`, uid 1000), Docker daemon access is denied, and real sudo requires a password. The marker nevertheless proves attacker-controlled command execution through the vulnerable product route when sudo does not consume stdin.

## Root Cause

Two defects combine:

1. **Authorization matcher omission.** In `9router@0.4.39`, the middleware matcher is an explicit allow-list:

   ```text
   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*"]
   ```

   `/api/tunnel/tailscale-install` is absent, so the dashboard guard is not invoked and the route accepts unauthenticated requests.

2. **Shell stdin injection.** The vulnerable install helper spawns `sudo -S sh`, writes `sudoPassword` to stdin first, then writes the legitimate install script. If sudo does not prompt, stdin remains available to `sh`; therefore a value such as `echo PWNED > /path/marker; id >> /path/marker; exit 0` is interpreted as the first shell command.

The fixed build uses a catch-all middleware matcher that protects `/api/tunnel/*`, validates the sudo password, writes the install script to a temporary file, and invokes `sh <tempfile>` so password bytes are no longer interpreted as shell script input.

## Reproduction Steps

1. Run `bundle/repro/reproduction_steps.sh`.
2. The script:
   - fetches `9router@0.4.39` and `9router@0.4.45`;
   - starts the real 9router server with `node cli.js --log --skip-update`;
   - waits for `/api/health`;
   - sends an unauthenticated `POST /api/tunnel/tailscale-install` request with JSON body containing an attacker command in `sudoPassword`;
   - records HTTP status, SSE response body, server log, sudo/no-prompt log, and marker files;
   - repeats two vulnerable attempts and two fixed attempts.
3. Expected evidence:
   - Vulnerable attempts: HTTP 200, response includes `Running install script...`, sudo/no-prompt log shows invocation as `sudo -S sh`, and marker files contain `PWNED_BY_9ROUTER_*` plus `uid=...` output.
   - Fixed attempts: HTTP 401 `{"error":"Unauthorized"}` and no marker files.

## Evidence

The script was run twice consecutively and succeeded both times. Key artifacts from the final run:

| Artifact | Evidence |
|---|---|
| `bundle/logs/reproduction_steps.log` | Complete successful transcript; final result confirmed. |
| `bundle/repro/runtime_manifest.json` | `service_started=true`, `healthcheck_passed=true`, `target_path_reached=true`; lists all proof artifacts. |
| `bundle/artifacts/vuln_1_http_code.txt`, `bundle/artifacts/vuln_2_http_code.txt` | `200` for vulnerable unauthenticated POSTs. |
| `bundle/logs/vuln_1_response.txt`, `bundle/logs/vuln_2_response.txt` | SSE progress includes `Running install script...`, proving route handler reached the install/spawn path. |
| `bundle/logs/vuln_1_sudo_shim.log`, `bundle/logs/vuln_2_sudo_shim.log` | No-prompt sudo precondition invoked as `argv=-S sh` and did not consume stdin. |
| `bundle/artifacts/vuln_1_marker.txt`, `bundle/artifacts/vuln_2_marker.txt` | Attacker command markers, e.g. `PWNED_BY_9ROUTER_vuln_1` and `uid=1000(vscode) ...`. |
| `bundle/artifacts/fixed_1_http_code.txt`, `bundle/artifacts/fixed_2_http_code.txt` | `401` for fixed unauthenticated POSTs. |
| `bundle/logs/fixed_1_response.txt`, `bundle/logs/fixed_2_response.txt` | `{"error":"Unauthorized"}`. |
| `bundle/artifacts/fixed_1_marker.txt`, `bundle/artifacts/fixed_2_marker.txt` | `[marker absent]`, proving the fixed build did not execute the command. |
| `bundle/artifacts/vuln_middleware_matcher.txt`, `bundle/artifacts/fixed_middleware_matcher.txt` | Vulnerable matcher omission vs fixed catch-all matcher. |
| `bundle/artifacts/vuln_install_code.txt`, `bundle/artifacts/fixed_install_code.txt` | Vulnerable stdin-to-`sh` implementation vs fixed temp-file/validation implementation. |

Key excerpt from `bundle/logs/reproduction_steps.log`:

```text
[vuln_1] HTTP 200; marker: PWNED_BY_9ROUTER_vuln_1 uid=1000(vscode) gid=1000(vscode) groups=1000(vscode)
[vuln_2] HTTP 200; marker: PWNED_BY_9ROUTER_vuln_2 uid=1000(vscode) gid=1000(vscode) groups=1000(vscode)
[fixed_1] HTTP 401; marker: [marker absent]
[fixed_2] HTTP 401; marker: [marker absent]
RESULT: CONFIRMED - attacker-controlled command executed through unauthenticated POST /api/tunnel/tailscale-install; fixed build blocked the same request.
```

## Recommendations / Next Steps

- Upgrade 9router to `>= 0.4.44` (or the latest patched release).
- Protect routes with a deny-by-default or catch-all authorization middleware matcher so newly added API endpoints cannot bypass authentication by omission.
- Never feed attacker-influenced data into a shell that reads its script from stdin. Pass scripts by immutable file path or avoid shell interpreters entirely.
- Validate privileged-operation inputs, including rejecting newlines/metacharacters in password fields.
- Add integration tests that enumerate `/api/*` routes and assert unauthenticated requests receive 401/403 unless deliberately public.

## Additional Notes

- The reproduction script is idempotent and was verified twice consecutively.
- The script attempts Docker-root and direct root/NOPASSWD modes before the controlled no-prompt sudo mode. Those stronger modes were unavailable in this sandbox (`docker` socket permission denied; no passwordless real sudo), but remain implemented for environments that can run the ticket's root/NOPASSWD precondition directly.
- The successful run still uses the real product HTTP boundary and real vulnerable route; only the sudo precondition was supplied by a local no-prompt executable so the constrained sandbox could demonstrate the final command-execution marker.
