CVE-2026-59800: Verified Repro With Script Download
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://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 No distinct bypass or alternate unauthenticated command-injection trigger was confirmed. The vulnerable control reached the original /api/tunnel/tailscale-install sudoPassword-to-sh sink, but path-normalization, adjacent tunnel daemon, and adjacent CLI-tools candidates did not create markers on the patched/latest targ…
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.
Variant Analysis & Alternative Triggers for CVE-2026-59800
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:
- 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. - 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 shand wrotesudoPasswordbefore the downloaded install script. The fixed/latest helper writes the install script to a temporary file and invokesshwith that file path, separating password stdin from script content.
Reviewed code paths include:
app/.next/server/middleware.jsin9router@0.4.39: matcher omits/api/tunnel/*.app/.next-cli-build/server/middleware.jsin9router@0.5.20: catch-all matcher.app/.next/server/chunks/3774.jsin9router@0.4.39: vulnerablespawn("sudo", ["-S", "sh"])install path.app/.next-cli-build/server/chunks/6457.jsin9router@0.5.20: install-script temp-file behavior.- Adjacent route bundles for
/api/tunnel/tailscale-start-daemonand/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:
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.
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.
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 causeshto interpretsudoPasswordbytes as script text.
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.
- 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 references0.4.44, but that version was not available from npm during this run; repro-stage evidence also tested0.4.45as 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.39created an attacker marker via the original route. No candidate created a marker on the fixed/latest target. - Parity:
nonefor 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, notshreading 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
- Run
bundle/vuln_variant/reproduction_steps.sh. - The script:
- resolves npm latest (
0.5.20) and records it inbundle/logs/vuln_variant/latest_version.txt; - fetches/unpacks
9router@0.4.39and9router@0.5.20; - starts the real 9router server for each target;
- supplies a PATH-local no-prompt sudo shim and a local install-script
curlshim 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
0only if a patched/latest bypass is confirmed, otherwise exits1after completing all attempts.
- resolves npm latest (
- 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
1is 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:
[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 version0.4.44was not present in npm registry; default patched control is0.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 ensuresudoPasswordis 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, andexecSyncusage.
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.jsonis required because no distinct variant/bypass was confirmed.
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.