# Variant RCA Report — CVE-2026-59800 / 9router

## Summary

No distinct bypass or alternate unauthenticated command-injection trigger was confirmed. The variant run reproduced the parent vulnerable control on `9router@0.4.39` through the real HTTP service and the same `sudoPassword -> sudo -S sh` sink, then tested path-normalization and adjacent privileged endpoints against the patched/latest target (`9router@0.5.20`, npm latest at runtime). The patched/latest build blocked unauthenticated requests before the privileged helpers or used code paths that do not interpret attacker-controlled password bytes as shell script input.

## Fix Coverage / Assumptions

The fix relies on two invariants:

1. **Authorization invariant:** privileged dashboard/API routes must be covered by middleware. The vulnerable matcher was an explicit allow-list that omitted `/api/tunnel/*`. The fixed/latest matcher is catch-all except static assets, so `/api/tunnel/*` and `/api/cli-tools/*` requests are checked before reaching privileged handlers.
2. **Shell-stdin invariant:** attacker-influenced password bytes must not be fed to a shell process that is reading its script from stdin. The vulnerable Linux installer spawned `sudo -S sh` and wrote `sudoPassword` before the downloaded install script. The fixed/latest helper writes the install script to a temporary file and invokes `sh` with that file path, separating password stdin from script content.

Reviewed code paths include:

- `app/.next/server/middleware.js` in `9router@0.4.39`: matcher omits `/api/tunnel/*`.
- `app/.next-cli-build/server/middleware.js` in `9router@0.5.20`: catch-all matcher.
- `app/.next/server/chunks/3774.js` in `9router@0.4.39`: vulnerable `spawn("sudo", ["-S", "sh"])` install path.
- `app/.next-cli-build/server/chunks/6457.js` in `9router@0.5.20`: install-script temp-file behavior.
- Adjacent route bundles for `/api/tunnel/tailscale-start-daemon` and `/api/cli-tools/antigravity-mitm`.

The npm package did not contain a `SECURITY.md` or equivalent threat-model document. The service exposes dashboard/API/tunnel functionality, so unauthenticated remote HTTP access to privileged API routes was treated as in scope.

## Variant / Alternate Trigger

Tested candidates:

1. **Parent baseline control:** `POST /api/tunnel/tailscale-install`
   - Vulnerable: HTTP 200; marker created; original shell sink reached.
   - Fixed/latest: HTTP 403 `Local only: CLI token required`; marker absent.
   - Status: parent control, not a variant.

2. **Path-normalization candidate:** `POST /api/tunnel/tailscale-install/`
   - Vulnerable: HTTP 308 redirect to canonical path; marker absent.
   - Fixed/latest: HTTP 308 redirect to canonical path; marker absent.
   - Status: not a bypass; following the redirect would only exercise the canonical route, which fixed/latest blocks.

3. **Adjacent tunnel daemon candidate:** `POST /api/tunnel/tailscale-start-daemon`
   - Vulnerable: HTTP 200 under the no-prompt sudo precondition, but marker absent.
   - Fixed/latest: HTTP 401 Unauthorized; marker absent.
   - Status: not the same sink. This route writes a password token to `sudo -S tailscaled ...`; it does not cause `sh` to interpret `sudoPassword` bytes as script text.

4. **Adjacent CLI-tools candidate:** `POST /api/cli-tools/antigravity-mitm`
   - Vulnerable: HTTP 401 Unauthorized; marker absent.
   - Fixed/latest: HTTP 403 `Local only: CLI token required`; marker absent.
   - Status: already protected in the vulnerable matcher and not a bypass of the tunnel-route fix.

## Impact

- **Package/component:** npm package `9router`; Next.js standalone server; privileged tunnel/Tailscale API handlers.
- **Affected parent version tested:** `9router@0.4.39`.
- **Patched/latest version tested:** `9router@0.5.20` (npm latest during this run). The ticket references `0.4.44`, but that version was not available from npm during this run; repro-stage evidence also tested `0.4.45` as a patched negative control.
- **Risk level and consequences for the parent:** critical unauthenticated remote OS command execution under the no-prompt sudo precondition.
- **Risk level for variants tested here:** none confirmed.

## Impact Parity

- **Disclosed/claimed maximum parent impact:** unauthenticated remote OS command execution.
- **Reproduced impact from this variant run:** the parent vulnerable control on `9router@0.4.39` created an attacker marker via the original route. No candidate created a marker on the fixed/latest target.
- **Parity:** `none` for variants/bypasses. The run demonstrated the parent control only to prove the test environment reached the sink.
- **Not demonstrated:** no distinct endpoint, path encoding, or adjacent route produced command execution on the patched/latest target.

## Root Cause

The parent root cause is a combination of missing middleware coverage for `/api/tunnel/tailscale-install` and unsafe stdin composition for `sudo -S sh`. A remote unauthenticated body field (`sudoPassword`) is written into a stream later interpreted by `sh` when sudo does not prompt.

The tested candidates did not reach the same underlying bug on the fixed/latest build:

- The catch-all matcher prevented unauthenticated tunnel/CLI requests from reaching privileged handlers.
- The trailing-slash path normalized to the canonical route rather than bypassing middleware.
- The daemon-start path uses sudo for `tailscaled`, not `sh` reading a script from stdin.
- The CLI-tools route is protected and is not part of the missing `/api/tunnel/*` matcher gap.

No fix commit URL was available from the ticket/package metadata in this run.

## Reproduction Steps

1. Run `bundle/vuln_variant/reproduction_steps.sh`.
2. The script:
   - resolves npm latest (`0.5.20`) and records it in `bundle/logs/vuln_variant/latest_version.txt`;
   - fetches/unpacks `9router@0.4.39` and `9router@0.5.20`;
   - starts the real 9router server for each target;
   - supplies a PATH-local no-prompt sudo shim and a local install-script `curl` shim to deterministically exercise the original sink without external Tailscale dependency;
   - tests the parent baseline and three candidate variants against both vulnerable and fixed/latest builds;
   - exits `0` only if a patched/latest bypass is confirmed, otherwise exits `1` after completing all attempts.
3. Expected evidence for this negative result:
   - Vulnerable parent baseline has HTTP 200 and a marker file.
   - Fixed/latest parent baseline and all variant candidates have no marker files.
   - The script completes without crashing; exit code `1` is expected for no confirmed variant.

## Evidence

Primary evidence:

- `bundle/logs/vuln_variant/reproduction_steps.log` — final runtime transcript.
- `bundle/vuln_variant/runtime_manifest.json` — runtime manifest with tested versions and proof artifacts.
- `bundle/vuln_variant/source_scan_summary.json` — matcher and sink observations.
- `bundle/vuln_variant/patch_analysis.md` — detailed fix analysis.
- `bundle/vuln_variant/validation_verdict.json` — structured negative verdict.

Key runtime excerpts from the final run:

```text
[vuln][c0_parent_install_baseline] HTTP=200 marker=present ... Running install script...
[vuln][c0_parent_install_baseline] vulnerable control reached original install shell sink
[fixed][c0_parent_install_baseline] HTTP=403 marker=absent response={"error":"Local only: CLI token required"}
[fixed][c1_install_trailing_slash] HTTP=308 marker=absent response=/api/tunnel/tailscale-install
[fixed][c2_start_daemon] HTTP=401 marker=absent response={"error":"Unauthorized"}
[fixed][c3_cli_tools_mitm] HTTP=403 marker=absent response={"error":"Local only: CLI token required"}
RESULT: NO DISTINCT VARIANT/BYPASS ...
```

Environment/version details:

- `bundle/logs/vuln_variant/latest_version.txt`: `npm_latest_version=0.5.20`.
- `bundle/logs/vuln_variant/fixed_version.txt`: ticket patched version `0.4.44` was not present in npm registry; default patched control is `0.4.45`.

## Recommendations / Next Steps

- Keep the catch-all middleware matcher and add automated route enumeration tests that assert all privileged `/api/*` endpoints reject unauthenticated remote requests unless explicitly public.
- Add regression tests for `/api/tunnel/tailscale-install`, including no-prompt sudo behavior, to ensure `sudoPassword` is never interpreted as shell script content.
- Avoid shell interpreters for privileged operations where possible. If a shell is required, pass immutable script file paths and validate any password/credential fields to reject newlines and shell metacharacters.
- Treat newly added tunnel, CLI-tool, and update endpoints as high-risk: require explicit authorization tests and review for `spawn`, `exec`, and `execSync` usage.

## Additional Notes

- The variant script was executed multiple times and completed deterministically. The last two required verification runs both completed and exited `1`, which is the documented no-variant outcome.
- Runtime proof uses the same controlled no-prompt sudo precondition strategy as the repro stage to model root/NOPASSWD/cached-sudo deployments in this sandbox.
- No `source_identity.json` is required because no distinct variant/bypass was confirmed.
