This is a locked lab door with an unguarded side entrance straight into the server terminal
CVE-2026-39987 is a missing-authentication flaw in marimo's terminal WebSocket endpoint, /terminal/ws, letting a remote attacker get an interactive shell without credentials. Authoritative sources put the affected range at versions before 0.23.0; the vendor release notes further narrow practical exposure to editable notebook deployments that are network-reachable, especially marimo edit --host 0.0.0.0, not app/run mode.
In pure technical terms the vendor's *critical* label is fair: this is pre-auth remote code execution with no user interaction. In fleet reality, severity comes down a notch because the vulnerable population is a subset of marimo installs: you need marimo deployed in edit mode, reachable over a network, and not fronted by stronger external auth. KEV listing and confirmed exploitation slam the brakes on any further downgrade, so this stays HIGH for enterprise patching priority.
5 steps from start to impact.
Find a reachable edit-mode marimo server
- A marimo server is bound to a reachable interface such as
0.0.0.0 - The instance is deployed in notebook edit mode rather than app/run mode
- Network path to the service exists from the attacker
- Most enterprises do not intentionally expose notebook editors to the public internet
- Reverse proxies, VPNs, private subnets, or bastions often remove unauthenticated reachability
- App/run mode is not affected
Open the terminal WebSocket without auth
/terminal/ws, typically via a custom Python exploit using websocket-client-style logic. The bug is simple and ugly: the route enforced mode/platform checks but skipped the auth validation used by other WebSocket endpoints.- The vulnerable code path exists (
marimo < 0.23.0) - The terminal endpoint is reachable from the attacker
- The attacker can complete a WebSocket handshake
- WAFs and reverse proxies sometimes block or constrain WebSocket upgrades
- Some orgs front marimo with stronger SSO/auth proxies, which removes the exposed path
- Non-default hardened deployments may never expose the endpoint externally
Land an interactive shell
- WebSocket auth bypass succeeds
- The marimo process can spawn or expose a usable shell environment
- Least-privilege containers, read-only filesystems, or stripped-down runtimes reduce post-exploitation options
- EDR/runtime sensors may catch suspicious child processes or reverse shells
- Process-level privilege may be non-root in better-managed deployments
.env, SSH keys, or cloud credential files.Harvest secrets from the notebook host
.env files, SSH keys, and cloud credentials, which is exactly why dev tooling RCEs age badly in real environments.- Useful secrets are stored on disk or in environment variables
- The marimo host has access to internal services, buckets, databases, or model infrastructure
- Short-lived credentials, isolated service accounts, and locked-down IAM sharply reduce payoff
- Secret managers with no local material on disk frustrate smash-and-grab collection
- Segmented egress can block immediate exfiltration or follow-on pivots
.env, ~/.ssh, cloud credential files, and unusual outbound connections shortly after WebSocket activity.Pivot into internal infrastructure
- Compromised credentials or network adjacency provide a second hop
- The marimo host has meaningful trust relationships inside the environment
- This is post-initial-access and depends on what the notebook can actually reach
- MFA, network segmentation, workload identity, and egress controls can break the chain
- Not every notebook host has privileged internal adjacency
The supporting signals.
| In the wild | Yes. NVD marks the CVE as present in CISA KEV, and Sysdig reported first exploitation 9 hours 41 minutes after public disclosure. |
|---|---|
| KEV status | Listed by CISA KEV on 2026-04-23 with due date 2026-05-07 in the NVD change history and KEV reference set. |
| Proof-of-concept availability | The advisory included enough detail for rapid weaponization; Sysdig says exploitation happened before a public PoC was available. Detection/exposure content now exists in ProjectDiscovery nuclei templates, so this is no longer obscure. |
| EPSS | 0.8071 from the user-supplied intel, which is extremely high and directionally consistent with KEV status and rapid exploitation. |
| CVSS / severity | User supplied CVSS v3.1 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H). GitHub also shows CVSS v4.0 9.3 Critical. Either way, the technical severity is plainly top-tier. |
| Affected range | Authoritative package advisory says marimo < 0.23.0. The vendor release notes say the practically exposed set is editable notebooks reachable on a shared network or the public internet. |
| Fixed version | 0.23.0 fixes the missing auth check on the terminal route. No distro backport evidence was found in the sources reviewed. |
| Exposure reality | This is not every marimo install. The release notes say run/app mode is not affected, and externally fronted auth proxies likely neutralize the path. That deployment friction is the main reason this is downgraded from fleet-wide CRITICAL to HIGH. |
| Observed attack activity | Sysdig later reported 662 exploit events from 11 unique source IPs across 10 countries between 2026-04-11 and 2026-04-14, including reverse shells, credential theft, DNS exfiltration, and malware delivery. |
| Researcher / reporting | The GitHub advisory credits q1uf3ng as reporter. Vendor fix landed in PR #9098 by mscolnick on 2026-04-08. |
noisgate verdict.
The decisive downward pressure is deployment friction: exploitation requires a reachable edit-mode marimo server, and the vendor explicitly says app/run mode is not affected. The decisive upward pressure is reality: it is KEV-listed and was exploited within hours, so any exposed instance should be treated as a likely initial-access path, not a theoretical bug.
Why this verdict
- Downgrade for reachability friction: the attacker needs a network-reachable marimo server in edit mode; app/run mode is explicitly out of scope, which cuts the exposed population hard.
- Downgrade for exposure population: marimo is a niche developer/data-science platform, not a ubiquitous enterprise control plane; many 10,000-host environments will have zero or very few installs.
- Downgrade for control assumptions: if the notebook is behind VPN, SSO proxy, private ingress, or segmentation, the unauthenticated remote precondition disappears.
- Upgrade back up for exploitation reality: CISA KEV plus exploitation in under 10 hours means exposed instances are not waiting for a sophisticated actor; they're already in commodity-attack territory.
- Upgrade back up for blast radius: notebook hosts routinely sit near cloud keys, SSH material, databases, and model assets, so one shell often turns into broader environment access.
Why not higher?
I am not calling this CRITICAL for fleet prioritization because the raw CVSS assumes universal reachability that simply is not true here. The exploit chain starts with a big environmental qualifier: exposed, editable marimo notebooks. In a normal enterprise, that is a much smaller slice than 'all hosts running vulnerable software.'
Why not lower?
I am not pushing this down to MEDIUM because the bug is still unauthenticated remote shell on exposed targets, with KEV listing and confirmed exploitation. Once the preconditions are met, there is very little attacker friction left and the post-exploitation value is unusually high for data-science infrastructure.
What to do — in priority order.
- Pull edit-mode marimo off reachable networks — Move any
marimo editdeployment behind VPN, private ingress, or a bastion immediately; if it is internet-facing, treat it as an emergency and do this within hours because KEV/active exploitation override the normal HIGH timeline. - Disable or block WebSocket access to
/terminal/ws— At the reverse proxy, ingress controller, or firewall layer, explicitly block the terminal WebSocket path until all instances are upgraded. This is the most direct compensating control and should be deployed within hours on exposed systems. - Force external auth in front of notebook editors — Put editable notebook access behind SSO, access proxy, or identity-aware proxy rather than relying on built-in app auth alone. Deploy this within hours for exposed services, and complete coverage well inside the normal 30-day HIGH mitigation window elsewhere.
- Hunt for secret access and child-process execution — Review runtime telemetry for shell spawns from marimo/Python, reads of
.envor~/.ssh, and unusual outbound traffic from notebook hosts. Because exploitation is confirmed, start hunting immediately on any host that was exposed after 2026-04-08. - Rotate co-located credentials — If a vulnerable exposed instance existed at any time after disclosure, rotate cloud keys, SSH keys, database passwords, and API tokens reachable from that host. Do this within hours for exposed instances because attacker dwell time in observed cases was measured in minutes.
- Relying on marimo's built-in token auth alone does not help on the vulnerable path; the bug is that
/terminal/wsskipped auth validation. - Waiting for routine vulnerability scans is not enough; first exploitation reportedly happened before CVE-based scanners had caught up.
- Perimeter-only HTTP rules are weak if they do not understand or block WebSocket upgrades to the terminal endpoint.
Crowdsourced verification payload.
Run this on the target Linux host or container that may be serving marimo, not from an auditor workstation. Invoke with sudo bash ./check_marimo_cve_2026_39987.sh or bash ./check_marimo_cve_2026_39987.sh if you already have permission to inspect processes; it needs local process visibility and benefits from root for full command-line inspection.
#!/usr/bin/env bash
# check_marimo_cve_2026_39987.sh
# Detects likely exposure to CVE-2026-39987 on Linux hosts.
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -u
have_cmd() { command -v "$1" >/dev/null 2>&1; }
verlt() {
# returns 0 if $1 < $2 using sort -V
[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" != "$2" ] && [ "$1" != "$2" ]
}
VERSION=""
SOURCE=""
# Try Python import first
for py in python3 python; do
if have_cmd "$py"; then
VERSION="$($py - <<'PY' 2>/dev/null
try:
import marimo
print(getattr(marimo, '__version__', ''))
except Exception:
pass
PY
)"
if [ -n "$VERSION" ]; then
SOURCE="python-import:$py"
break
fi
fi
done
# Fallback to pip metadata
if [ -z "$VERSION" ]; then
for pipcmd in "python3 -m pip" "python -m pip" pip3 pip; do
if sh -c "$pipcmd --version" >/dev/null 2>&1; then
VERSION="$(sh -c "$pipcmd show marimo 2>/dev/null | awk -F': ' '/^Version:/{print \$2; exit}'")"
if [ -n "$VERSION" ]; then
SOURCE="pip-show:$pipcmd"
break
fi
fi
done
fi
if [ -z "$VERSION" ]; then
echo "UNKNOWN - marimo package version not found"
exit 2
fi
# Determine if version is fixed
if verlt "$VERSION" "0.23.0"; then
PKG_STATE="vulnerable"
else
echo "PATCHED - marimo version $VERSION ($SOURCE) is >= 0.23.0"
exit 0
fi
# Inspect processes for risky runtime posture
RISKY_PROC=0
RISKY_DETAILS=""
if have_cmd ps; then
while IFS= read -r line; do
cmd="$line"
echo "$cmd" | grep -E '(^|[ /])marimo([ ]|$)' >/dev/null 2>&1 || continue
echo "$cmd" | grep -E '([ ]|^)edit([ ]|$)|marimo[ ]+edit' >/dev/null 2>&1 || continue
if echo "$cmd" | grep -E -- '--host[ =](0\.0\.0\.0|::)| -h[ ]+(0\.0\.0\.0|::)' >/dev/null 2>&1; then
RISKY_PROC=1
RISKY_DETAILS="$RISKY_DETAILS\n$cmd"
elif ! echo "$cmd" | grep -E -- '--host[ =](127\.0\.0\.1|localhost)| -h[ ]+(127\.0\.0\.1|localhost)' >/dev/null 2>&1; then
# No explicit localhost binding seen; still suspicious for edit mode
RISKY_PROC=1
RISKY_DETAILS="$RISKY_DETAILS\n$cmd"
fi
done < <(ps -eo args= 2>/dev/null)
fi
# Inspect listening sockets on the common marimo port if possible
PORT2718=0
if have_cmd ss; then
ss -lntp 2>/dev/null | grep -E '[:.]2718[[:space:]]' >/dev/null 2>&1 && PORT2718=1
elif have_cmd netstat; then
netstat -lntp 2>/dev/null | grep -E '[:.]2718[[:space:]]' >/dev/null 2>&1 && PORT2718=1
fi
if [ "$RISKY_PROC" -eq 1 ]; then
echo "VULNERABLE - marimo version $VERSION (<0.23.0) with likely exposed edit-mode process detected.${RISKY_DETAILS}"
exit 1
fi
if [ "$PORT2718" -eq 1 ]; then
echo "VULNERABLE - marimo version $VERSION (<0.23.0); listening on common marimo port 2718, exposure should be reviewed immediately"
exit 1
fi
echo "UNKNOWN - marimo version $VERSION is vulnerable (<0.23.0), but this script could not confirm an exposed edit-mode runtime. Review running processes, ingress, and reverse-proxy paths manually."
exit 2
If you remember one thing.
/terminal/ws access within hours instead of waiting for the standard noisgate mitigation SLA. For the actual upgrade, move all remaining vulnerable marimo < 0.23.0 instances to 0.23.0+ on an accelerated schedule; the default HIGH noisgate remediation SLA is ≤180 days, but exposed edit-mode systems should be upgraded as part of the same immediate response window, not left to ride the long tail.Sources
- NVD CVE-2026-39987
- GitHub Advisory GHSA-2679-6mx9-h9xc
- marimo 0.23.0 release notes
- marimo PR #9098 fix: properly authenticate terminal route
- Sysdig: From disclosure to exploitation in under 10 hours
- Sysdig: weaponized for blockchain botnet via Hugging Face
- Endor Labs: Root in One Request
- ProjectDiscovery nuclei templates release
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.