# REPRO-2026-00316: marimo Pre-Auth RCE via Terminal WebSocket Authentication Bypass (/terminal/ws missing validate_auth) ## Summary Status: published Severity: critical CVSS: Unknown CWE: CWE-306 (Missing Authentication for Critical Function) Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00316 CVE: CVE-2026-39987 ## Package Name: marimo Ecosystem: github Affected: <0.23.0 Fixed: 0.23.0 ## Root Cause # RCA Report — CVE-2026-39987: marimo Pre-Auth RCE via /terminal/ws ## Summary marimo's interactive terminal WebSocket endpoint (`/terminal/ws`) completely skipped authentication validation. marimo relies on Starlette's `AuthenticationMiddleware`, which only *marks* failed-auth connections as `UnauthenticatedUser` without actively rejecting WebSocket connections; real enforcement depends on endpoint-level checks. While the main `/ws` endpoint validates credentials, `/terminal/ws` had neither a `@requires("edit")` decorator nor a `validate_auth()` call, so an unauthenticated attacker could open a WebSocket and be handed a full interactive PTY shell running with the privileges of the marimo process — pre-authentication remote code execution. ## Impact - **Package/component:** `marimo` (Python notebook server), `marimo/_server/api/endpoints/terminal.py` — `/terminal/ws` WebSocket endpoint. - **Affected versions:** all versions `< 0.23.0` (verified on `0.22.5`). - **Risk level:** Critical (CVSS 9.3, EPSS 0.953, CISA KEV 2026-04-23, exploited in the wild). Consequences: unauthenticated remote attacker obtains an interactive OS shell with the marimo process's privileges (frequently root in Docker deployments), enabling reconnaissance, credential theft (e.g. `.env` cloud keys), lateral movement, and full host compromise. ## Impact Parity - **Disclosed/claimed maximum impact:** pre-authentication remote code execution (interactive PTY shell, arbitrary OS commands). - **Reproduced impact from this run:** identical — from a raw, credential-less WebSocket client we obtained a PTY shell and executed arbitrary commands (`echo PRUVA_VULN_A_$(id -u)_$(id -un)`), observing marker output `PRUVA_VULN_A1_1000_vscode` proving execution as the marimo server user (uid 1000). - **Parity:** `full`. - **Not demonstrated:** nothing material — the claim is fully reproduced, including the fixed-version negative control. ## Root Cause `marimo/_server/api/endpoints/terminal.py::websocket_endpoint` (vulnerable code at fix-commit parent `c24d4806398f30be6b12acd6c60d1d7c68cfd12a^`) performed only two checks before `websocket.accept()` and `pty.fork()`: 1. `app_state.mode != SessionMode.EDIT` → close. 2. `supports_terminal()` → close. There was **no authentication check**. Because Starlette's `AuthenticationMiddleware` does not reject unauthenticated WebSocket upgrades (it only attaches an `UnauthenticatedUser`), the absence of an explicit `validate_auth(websocket)` call meant anyone could reach the PTY-spawning code. Fix commit `c24d4806398f30be6b12acd6c60d1d7c68cfd12a` (PR #9098, released in 0.23.0) adds exactly: ```python from marimo._server.api.auth import validate_auth ... if app_state.enable_auth and not validate_auth(websocket): await websocket.close(WebSocketCodes.UNAUTHORIZED, "MARIMO_UNAUTHORIZED") return ``` aligning `/terminal/ws` with the auth validation used by the other WebSocket endpoints. Verified in this run: the patch hunk exists at the fixed commit, the parent commit lacks it, the installed 0.22.5 package lacks `validate_auth` in `terminal.py`, and the installed 0.23.0 package contains it. ## Reproduction Steps 1. `bundle/repro/reproduction_steps.sh` (helper: `bundle/repro/ws_exploit_client.py`). 2. The script: - clones/uses the marimo source checkout and verifies the fix patch hunk; - creates two virtualenvs: `marimo==0.22.5` (vulnerable) and `marimo==0.23.0` (fixed); - starts each real server with token auth enabled (`marimo edit --headless --token-password topsecretpw`); - proves the HTTP auth gate is active (unauthenticated `/` → HTTP 303 login redirect); - as an unauthenticated attacker, opens a raw WebSocket to `/terminal/ws` (no token/cookie/header) and sends a shell command — **twice** against the vulnerable build (both yield PTY output with the unique marker) and **twice** against the fixed build (both rejected with HTTP 403 during the WS upgrade); - runs an authenticated positive control on the fixed build (valid `access_token` → terminal works), proving the fix blocks only unauthenticated access; - writes `bundle/repro/runtime_manifest.json`. 3. Expected evidence: vulnerable attempts print `RCE_CONFIRMED` with marker `PRUVA_VULN_A__` in PTY output; fixed attempts print `CONNECT_FAILED: InvalidStatus: server rejected WebSocket connection: HTTP 403`. ## Evidence - `bundle/logs/reproduction_steps.log` — full run transcript (verdict line: `vuln RCE attempts OK=2/2, fixed rejects OK=2/2, fixed auth control=1`). - `bundle/logs/server_vuln.log`, `bundle/logs/server_fixed.log` — server startup showing token auth (`URL: http://localhost:2718?access_token=topsecretpw`). - `bundle/logs/vuln_unauth_attempt1.log` / `...attempt2.log` — key excerpt: ``` echo PRUVA_VULN_A1_$(id -u)_$(id -un) vscode ➜ /tmp $ echo PRUVA_VULN_A1_$(id -u)_$(id -un) PRUVA_VULN_A1_1000_vscode RESULT: RCE_CONFIRMED marker observed in PTY output ``` (unauthenticated WS accepted → interactive shell → arbitrary command executed as uid 1000 `vscode`, the marimo process user). - `bundle/logs/fixed_unauth_attempt1.log` / `...attempt2.log` — `CONNECT_FAILED: InvalidStatus: server rejected WebSocket connection: HTTP 403`. - `bundle/logs/fixed_auth_control.log` — authenticated request on the fixed build still obtains the terminal (`PRUVA_FIXED_AUTH_1000_vscode`). - `bundle/logs/patch_hunk.txt` — the added `validate_auth` lines from the fix commit. - `bundle/repro/runtime_manifest.json` — `entrypoint_kind=endpoint`, `service_started=true`, `healthcheck_passed=true`, `target_path_reached=true`. - Environment: Python 3.14.4, pip-installed `marimo==0.22.5` / `marimo==0.23.0`, `websockets` client library, Linux x86_64. Script verified idempotent by two consecutive successful runs (exit 0 both times). ## Recommendations / Next Steps - **Upgrade** to marimo ≥ 0.23.0 immediately (fix: PR #9098 / commit `c24d4806398f30be6b12acd6c60d1d7c68cfd12a`). - **Fix approach (already upstream):** call `validate_auth(websocket)` and close with `WebSocketCodes.UNAUTHORIZED` before `websocket.accept()` whenever `enable_auth` is true — for every WebSocket endpoint, not just `/ws`. - **Defense in depth:** never expose `marimo edit` to untrusted networks; put it behind an authenticating reverse proxy; run it as an unprivileged user; audit any deployment that ran < 0.23.0 with a reachable port for compromise (unexpected PTY child processes, shell history, `.env` access). - **Testing:** add a regression test asserting unauthenticated `/terminal/ws` upgrades are rejected (upstream added one in `tests/_server/api/endpoints/test_terminal.py`). ## Additional Notes - **Idempotency:** the script is fully self-contained (installs its own venvs, manages server lifecycle with bounded waits and process-group cleanup) and passed twice consecutively with exit 0. - **Edge cases:** the vulnerability requires edit mode (`SessionMode.EDIT`, i.e. `marimo edit`, the default) and a POSIX platform with `pty` support — both are the standard deployment shape. Auth must be enabled (non-empty token), which is marimo's default when a token is generated or `--token-password` is set; with auth disabled the impact is identical but by design. - The advisory body text ("<= 0.20.4") understates the range; the structured range `< 0.23.0` is correct — 0.22.5 was confirmed vulnerable here. ## Reproduction Details Reproduced: 2026-07-30T07:54:02.195Z Duration: 1459 seconds Tool calls: 134 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00316 pruva-verify CVE-2026-39987 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00316&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00316/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-39987 - Source: https://github.com/marimo-team/marimo/security/advisories/GHSA-2679-6mx9-h9xc ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 10606 bytes) - bundle/repro/rca_report.md (analysis, 7548 bytes) - bundle/repro/ws_exploit_client.py (script, 1923 bytes) - bundle/repro/runtime_manifest.json (other, 681 bytes) - bundle/logs/reproduction_steps.log (log, 4579 bytes) - bundle/logs/vuln_unauth_attempt1.log (log, 317 bytes) - bundle/logs/fixed_unauth_attempt1.log (log, 78 bytes) - bundle/logs/server_vuln.log (log, 258 bytes) - bundle/logs/server_fixed.log (log, 258 bytes) - bundle/logs/vuln_unauth_attempt2.log (log, 317 bytes) - bundle/logs/fixed_unauth_attempt2.log (log, 78 bytes) - bundle/logs/fixed_auth_control.log (log, 326 bytes) - bundle/logs/patch_hunk.txt (other, 115 bytes) - bundle/repro/validation_verdict.json (other, 842 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00316 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00316/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/reproductions/REPRO-2026-00316 ## 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