# REPRO-2026-00270: 9router before 0.4.44 allows unauthenticated remote OS command execution via the /api/tunnel/tailscale-install endpoint by injecting shell commands in the sudoPassword field when sudo does not prompt for a password. ## Summary Status: published Severity: critical Type: security Confidence: medium ## Identifiers REPRO ID: REPRO-2026-00270 CVE: CVE-2026-59800 ## Package Name: 9router (npm) Ecosystem: npm Affected: < 0.4.44 (advisory states <= v0.4.39) Fixed: 0.4.44 (GitHub advisory page); OSV lists 0.4.45 as fixed range ## Root Cause # 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 ` 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. ## Reproduction Details Reproduced: 2026-07-08T04:50:04.999Z Duration: 2753 seconds Tool calls: 382 Turns: Unknown Handoffs: 4 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00270 pruva-verify CVE-2026-59800 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00270&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00270/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-59800 - Source: https://nvd.nist.gov/vuln/detail/CVE-2026-59800 ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 21179 bytes) - bundle/repro/rca_report.md (analysis, 8522 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 11981 bytes) - bundle/vuln_variant/rca_report.md (analysis, 9241 bytes) - bundle/artifact_promotion_manifest.json (other, 27506 bytes) - bundle/artifact_promotion_report.json (other, 37865 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 906 bytes) - bundle/repro/validation_verdict.json (other, 798 bytes) - bundle/repro/runtime_manifest.json (other, 1342 bytes) - bundle/logs/demo_response.txt (other, 252 bytes) - bundle/logs/reproduction_steps.log (log, 2016 bytes) - bundle/logs/vuln_1_response.txt (other, 293 bytes) - bundle/logs/fixed_1_response.txt (other, 24 bytes) - bundle/logs/vuln_1_server.log (log, 1890 bytes) - bundle/logs/vuln_1_sudo_shim.log (log, 389 bytes) - bundle/logs/vuln_2_response.txt (other, 293 bytes) - bundle/logs/vuln_2_sudo_shim.log (log, 389 bytes) - bundle/logs/fixed_1_server.log (log, 1539 bytes) - bundle/logs/fixed_2_response.txt (other, 24 bytes) - bundle/logs/vuln_variant/reproduction_steps.log (log, 2289 bytes) - bundle/vuln_variant/validation_verdict.json (other, 3215 bytes) - bundle/vuln_variant/variant_manifest.json (other, 3818 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 6950 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 2490 bytes) - bundle/logs/vuln_variant/fixed_c0_parent_install_baseline_http_code.txt (other, 4 bytes) - bundle/logs/vuln_variant/fixed_c0_parent_install_baseline_marker.txt (other, 16 bytes) - bundle/logs/vuln_variant/fixed_c0_parent_install_baseline_response.txt (other, 42 bytes) - bundle/logs/vuln_variant/fixed_c1_install_trailing_slash_http_code.txt (other, 4 bytes) - bundle/logs/vuln_variant/fixed_c1_install_trailing_slash_marker.txt (other, 16 bytes) - bundle/logs/vuln_variant/fixed_c1_install_trailing_slash_response.txt (other, 29 bytes) - bundle/logs/vuln_variant/fixed_c2_start_daemon_http_code.txt (other, 4 bytes) - bundle/logs/vuln_variant/fixed_c2_start_daemon_marker.txt (other, 16 bytes) - bundle/logs/vuln_variant/fixed_c2_start_daemon_response.txt (other, 24 bytes) - bundle/logs/vuln_variant/fixed_c3_cli_tools_mitm_http_code.txt (other, 4 bytes) - bundle/logs/vuln_variant/fixed_c3_cli_tools_mitm_marker.txt (other, 16 bytes) - bundle/logs/vuln_variant/fixed_c3_cli_tools_mitm_response.txt (other, 42 bytes) - bundle/logs/vuln_variant/fixed_server.log (log, 1161 bytes) - bundle/logs/vuln_variant/fixed_version.txt (other, 146 bytes) - bundle/logs/vuln_variant/latest_version.txt (other, 26 bytes) - bundle/logs/vuln_variant/route_files.log (log, 3710 bytes) - bundle/logs/vuln_variant/shell_sink_scan.log (log, 31150 bytes) - bundle/logs/vuln_variant/vuln_c0_parent_install_baseline_http_code.txt (other, 4 bytes) - bundle/logs/vuln_variant/vuln_c0_parent_install_baseline_marker.txt (other, 100 bytes) - bundle/logs/vuln_variant/vuln_c0_parent_install_baseline_response.txt (other, 293 bytes) - bundle/logs/vuln_variant/vuln_c1_install_trailing_slash_http_code.txt (other, 4 bytes) - bundle/logs/vuln_variant/vuln_c1_install_trailing_slash_marker.txt (other, 16 bytes) - bundle/logs/vuln_variant/vuln_c1_install_trailing_slash_response.txt (other, 29 bytes) - bundle/logs/vuln_variant/vuln_c2_start_daemon_http_code.txt (other, 4 bytes) - bundle/logs/vuln_variant/vuln_c2_start_daemon_marker.txt (other, 16 bytes) - bundle/logs/vuln_variant/vuln_c2_start_daemon_response.txt (other, 16 bytes) - bundle/logs/vuln_variant/vuln_c3_cli_tools_mitm_http_code.txt (other, 4 bytes) - bundle/logs/vuln_variant/vuln_c3_cli_tools_mitm_marker.txt (other, 16 bytes) - bundle/logs/vuln_variant/vuln_c3_cli_tools_mitm_response.txt (other, 24 bytes) - bundle/logs/vuln_variant/vuln_server.log (log, 1658 bytes) - bundle/logs/vuln_variant/vuln_sudo_shim.log (log, 1781 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00270 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00270/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00270 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev