Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-39987-Lab — Local Docker lab reproducing CVE-2026-39987, a pre-auth RCE in marimo's terminal WebSocket. Compares vulnerable and patched versions with least-harm PoC scripts for educational security research. | Kitploit
Tools/GitHubGitHub/rootdirective-sec/cve-2026-39987-lab
Container SecurityVulnerability AnalysisExploitationWeb SecurityLearning & EducationLabs & Practice
GitHubrootdirective-sec/cve-2026-39987-lab

CVE-2026-39987-Lab

Local Docker lab reproducing CVE-2026-39987, a pre-auth RCE in marimo's terminal WebSocket. Compares vulnerable and patched versions with least-harm PoC scripts for educational security research.

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share
View Repository
1335 months agoNot yet reviewed

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:

/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:

ServiceVersionURLExpected behavior
vulnmarimo 0.20.4http://127.0.0.1:8081/terminal/ws accepts unauthenticated WebSocket connection
patchedmarimo 0.23.0http://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:

/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:

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:

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

.
├── 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

FilePurpose
docker-compose.ymlDefines vulnerable and patched marimo services
vuln/DockerfileBuilds the vulnerable marimo service
patched/DockerfileBuilds the patched marimo service
vuln/notebook.pyMinimal marimo notebook file used by the vulnerable service
patched/notebook.pyMinimal marimo notebook file used by the patched service
poc/poc.pyLeast-harm proof script that runs benign commands only
poc/rce_poc.pyReal-time learning client for observing terminal behavior in the local lab
poc/requirements.txtPython dependencies for PoC scripts
SAFETY.mdSafety rules and scope boundaries

Lab Architecture

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:

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:

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

- Linux x86_64 or macOS with Docker Desktop
- Docker Compose v2
- Python 3.9+
- Localhost-only testing

Required tools:

docker --version
docker compose version
python3 --version

Setup

Clone or enter the project directory:

cd cve-2026-39987

Build and start both services:

docker compose up -d --build

Check that both containers are running:

docker compose ps

Expected services:

cve-2026-39987-vuln
cve-2026-39987-patched

Verify Versions

Check the vulnerable service version:

docker compose exec vuln marimo --version

Expected:

0.20.4

Check the patched service version:

docker compose exec patched marimo --version

Expected:

0.23.0

Install PoC Dependencies

Create a Python virtual environment:

python3 -m venv .venv
source .venv/bin/activate

Install dependencies:

python -m pip install -r poc/requirements.txt

Least-Harm PoC

The primary proof script is:

poc/poc.py

It attempts to connect to:

/terminal/ws

Then it sends only benign proof commands:

id
whoami
hostname

No reverse shell, file write, persistence, credential access, or destructive command is used.

Test vulnerable service

Run:

python poc/poc.py --base-url http://127.0.0.1:8081

Expected result:

[*] 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
<container-hostname>

If the script prints raw terminal output but includes this block, the vulnerability is still confirmed:

Download Tool