# Patch Analysis — CVE-2026-59800 Variant Stage

## Scope reviewed

- Product: npm package `9router`.
- Submitted vulnerable target: `9router@0.4.39`.
- Ticket patched version: `0.4.44`; `npm pack 9router@0.4.44` returned `ETARGET` during this run, so the runtime fixed/latest target was the currently published npm latest, `9router@0.5.20`. The repro-stage negative control also used `0.4.45`.
- Primary files inspected in packaged builds:
  - Vulnerable middleware bundle: `app/.next/server/middleware.js`.
  - Fixed/latest middleware bundle: `app/.next-cli-build/server/middleware.js`.
  - Vulnerable Tailscale helper bundle: `app/.next/server/chunks/3774.js`.
  - Fixed/latest Tailscale helper bundle: `app/.next-cli-build/server/chunks/6457.js`.
  - API route bundles under `app/.next*/server/app/api/tunnel/*` and `app/.next*/server/app/api/cli-tools/*`.
- Threat model/security docs: no `SECURITY.md` or equivalent security policy file was present in the npm package. The product README describes a dashboard and remote API/tunnel functionality; unauthenticated remote dashboard/API access is therefore treated as crossing the service trust boundary.

## What the fix changes

1. **Middleware authorization coverage changed from allow-list to catch-all.**

   In `0.4.39`, the middleware matcher is an explicit 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/:path*` is absent, so unauthenticated callers can reach tunnel handlers directly.

   In the fixed/latest build, the matcher is effectively catch-all except static assets:

   ```text
   matcher:["/((?!_next/static|_next/image|favicon\\.ico).*)"]
   ```

   Runtime evidence confirms unauthenticated requests to patched/latest tunnel and CLI-tool candidates are rejected before the privileged operation: the original install route returned `403 {"error":"Local only: CLI token required"}`, the start-daemon route returned `401 {"error":"Unauthorized"}`, and the CLI tools route returned `403 {"error":"Local only: CLI token required"}`.

2. **Install-script stdin handling changed.**

   The vulnerable Linux install helper downloads the Tailscale install script, then runs:

   ```js
   spawn("sudo", ["-S", "sh"], {stdio:["pipe","pipe","pipe"]})
   child.stdin.write(`${sudoPassword} `)
   child.stdin.write(downloadedInstallScript)
   ```

   When sudo does not consume stdin, `sh` reads the attacker-supplied `sudoPassword` as shell script text before the legitimate install script.

   In the fixed/latest helper, the install script is written to a temporary file (`writeFileSync(...)` in `app/.next-cli-build/server/chunks/6457.js`) and the shell is invoked with a file path (`sudo -S sh <tempfile>` equivalent in minified form). This separates password stdin from shell script content.

3. **Adjacent privileged routes remain present but are behind authorization.**

   Routes such as `/api/tunnel/tailscale-start-daemon` and `/api/cli-tools/antigravity-mitm` still parse `sudoPassword` fields and can spawn privileged commands, but they either do not interpret `sudoPassword` as shell input or are protected by the expanded middleware/CLI-token checks.

## Assumptions made by the fix

- All sensitive HTTP routes should pass through the catch-all dashboard/CLI guard unless deliberately public static assets.
- The privileged Tailscale install path must not pass attacker-controlled password bytes to a shell that is reading its script from stdin.
- Requests from unauthenticated remote clients should be rejected before the route body reaches privileged helpers.
- Local/CLI-only administrative routes may continue to accept privileged inputs if the local CLI token or dashboard authorization is present.

## Code paths and inputs not covered / variant candidates evaluated

The variant search focused on paths that either could still reach the same `sudo -S sh` sink or could exploit the same missing-auth trust boundary:

1. **Path normalization / trailing slash on the original endpoint** (`POST /api/tunnel/tailscale-install/`).
   - Vulnerable and fixed/latest both returned `308` redirect to the canonical path and did not execute a marker.
   - This is not a bypass; following the redirect would only exercise the already-tested canonical route, which the fixed/latest build blocks.

2. **Adjacent tunnel daemon endpoint** (`POST /api/tunnel/tailscale-start-daemon`).
   - Vulnerable build returned `200 {"success":true}` under the no-prompt sudo precondition, but no attacker marker was created because this path spawns `sudo -S tailscaled ...` and writes only the password token, not a shell script.
   - Fixed/latest build returned `401 Unauthorized`.
   - This is not the same command-injection sink and is not a bypass.

3. **Adjacent CLI tools / MITM privileged endpoint** (`POST /api/cli-tools/antigravity-mitm`).
   - Vulnerable build returned `401 Unauthorized`; this route was already included in the vulnerable matcher.
   - Fixed/latest build returned `403 Local only: CLI token required`.
   - This route does not provide an unauthenticated path to the disclosed sink.

Additional `/api/tunnel/*` endpoints were reviewed by route listing and source scan. The only route that combines unauthenticated access in the vulnerable build with `sudoPassword` interpreted by `sh` stdin is the original `/api/tunnel/tailscale-install` path. The materially different routes either lack shell-script stdin interpretation or are protected.

## Behavior before and after the fix

Runtime script: `bundle/vuln_variant/reproduction_steps.sh` (executed twice successfully; expected exit code is `1` because no bypass was found).

- Vulnerable control (`9router@0.4.39`, canonical install route): HTTP 200, SSE progress includes `Running install script...`, and the marker file is created. This confirms the test environment reaches the original command-injection sink.
- Fixed/latest control (`9router@0.5.20`, canonical install route): HTTP 403 and no marker.
- Variant candidates on fixed/latest: no marker creation; responses were 308 redirect, 401, or 403.

## Completeness assessment

The fix appears complete for the disclosed unauthenticated remote command-injection class and the searched neighboring surfaces. It closes both halves of the bug: the authorization omission is addressed by a catch-all matcher, and the shell-stdin injection is addressed by no longer feeding attacker-controlled password bytes into a shell that reads its script from stdin.

The only residual risk is future regression: new privileged endpoints could be added outside the catch-all guard or could reintroduce shell stdin parsing. The fix should be backed by route enumeration tests and by unit/integration tests that reject unauthenticated access to all privileged `/api/*` routes.
