CVE-2026-59800: Verified Reproduction
CVE-2026-59800: 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.
CVE-2026-59800 is verified against 9router (npm) · npm. Affected versions: < 0.4.44 (advisory states <= v0.4.39). Fixed in 0.4.44 (GitHub advisory page); OSV lists 0.4.45 as fixed range. Vulnerability class: Command Injection. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00270.
What Is CVE-2026-59800?
CVE-2026-59800 is a critical unauthenticated OS command injection (CWE-78) in the npm package 9router, reachable through the /api/tunnel/tailscale-install endpoint. Pruva reproduced it (reproduction REPRO-2026-00270).
CVE-2026-59800 Severity & CVSS Score
CVE-2026-59800 is rated critical severity, with a CVSS base score of 9.2 out of 10.
Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.
Affected 9router (npm) Versions
9router (npm) · npm versions < 0.4.44 (advisory states <= v0.4.39) are affected.
How to Reproduce CVE-2026-59800
pruva-verify REPRO-2026-00270 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00270/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-59800
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- medium confidence
- the upstream fix blocks the same trigger
unauthenticated JSON POST body field sudoPassword containing shell commands
- POST /api/tunnel/tailscale-install
- install helper
- spawn('sudo', ['-S','sh'])
- sudoPassword reaches sh stdin when sudo does not prompt
reproduction_steps.sh How the agent worked
Root Cause and Exploit Chain for CVE-2026-59800
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.
- Package / component: npm package
9router; Next.js standalone server route/api/tunnel/tailscale-install; Linux Tailscale install helper. - Affected versions:
< 0.4.44; reproduced against9router@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-installendpoint on the vulnerable build. The reproduction script ran the actual 9router npm package and actual Next.js server, passed/api/health, sent JSON containingsudoPassword, reached the vulnerablespawn("sudo", ["-S", "sh"])path, and wrote marker artifacts containingPWNED_BY_9ROUTER_*plusidoutput. 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=0output 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:
Authorization matcher omission. In
9router@0.4.39, the middleware matcher is an explicit allow-list: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-installis absent, so the dashboard guard is not invoked and the route accepts unauthenticated requests.Shell stdin injection. The vulnerable install helper spawns
sudo -S sh, writessudoPasswordto stdin first, then writes the legitimate install script. If sudo does not prompt, stdin remains available tosh; therefore a value such asecho PWNED > /path/marker; id >> /path/marker; exit 0is 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
- Run
bundle/repro/reproduction_steps.sh. - The script:
- fetches
9router@0.4.39and9router@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-installrequest with JSON body containing an attacker command insudoPassword; - records HTTP status, SSE response body, server log, sudo/no-prompt log, and marker files;
- repeats two vulnerable attempts and two fixed attempts.
- fetches
- Expected evidence:
- Vulnerable attempts: HTTP 200, response includes
Running install script..., sudo/no-prompt log shows invocation assudo -S sh, and marker files containPWNED_BY_9ROUTER_*plusuid=...output. - Fixed attempts: HTTP 401
{"error":"Unauthorized"}and no marker files.
- Vulnerable attempts: HTTP 200, response includes
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:
[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 (
dockersocket 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.
CVE-2026-59800 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for CVE-2026-59800
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-59800
Upgrade 9router (npm) · npm to 0.4.44 (GitHub advisory page); OSV lists 0.4.45 as fixed range or later.
FAQ: CVE-2026-59800
What conditions are needed for the CVE-2026-59800 attack to work?
Which 9router versions are affected by CVE-2026-59800, and where is it fixed?
How severe is CVE-2026-59800?
How can I reproduce CVE-2026-59800?
References for CVE-2026-59800
Authoritative sources for CVE-2026-59800 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.