CVE-2026-39987
Description
marimo is a reactive Python notebook. Prior to 0.23.0, Marimo has a Pre-Auth RCE vulnerability. The terminal WebSocket endpoint /terminal/ws lacks authentication validation, allowing an unauthenticated attacker to obtain a full PTY shell and execute arbitrary system commands. Unlike other WebSocket endpoints (e.g., /ws) that correctly call validate_auth() for authentication, the /terminal/ws endpoint only checks the running mode and platform support before accepting connections, completely skipping authentication verification. This vulnerability is fixed in 0.23.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
marimoPyPI | < 0.23.0 | 0.23.0 |
Affected products
1Patches
1c24d4806398ffix: properly authenticate terminal route (#9098)
2 files changed · +31 −4
marimo/_server/api/endpoints/terminal.py+9 −0 modified@@ -14,7 +14,9 @@ from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState from marimo import _loggers +from marimo._server.api.auth import validate_auth from marimo._server.api.deps import AppState +from marimo._server.codes import WebSocketCodes from marimo._server.router import APIRouter from marimo._session.model import SessionMode from marimo._utils.platform import is_pyodide, is_windows @@ -353,6 +355,13 @@ def supports_terminal() -> bool: @router.websocket("/ws") async def websocket_endpoint(websocket: WebSocket) -> None: app_state = AppState(websocket) + + if app_state.enable_auth and not validate_auth(websocket): + await websocket.close( + WebSocketCodes.UNAUTHORIZED, "MARIMO_UNAUTHORIZED" + ) + return + if app_state.mode != SessionMode.EDIT: await websocket.close( code=1008, reason="Terminal only available in edit mode"
tests/_server/api/endpoints/test_terminal.py+22 −4 modified@@ -24,6 +24,8 @@ from marimo._session.model import SessionMode from tests._server.mocks import get_session_manager +TERMINAL_WS_URL = "/terminal/ws?access_token=fake-token" + if TYPE_CHECKING: from starlette.testclient import TestClient @@ -33,7 +35,7 @@ @pytest.mark.skipif(is_windows, reason="Skip on Windows") def test_terminal_ws(client: TestClient) -> None: - with client.websocket_connect("/terminal/ws") as websocket: + with client.websocket_connect(TERMINAL_WS_URL) as websocket: # Send echo message websocket.send_text("echo hello") data = websocket.receive_text() @@ -44,11 +46,27 @@ def test_terminal_ws_not_allowed_in_run(client: TestClient) -> None: session_manager: SessionManager = get_session_manager(client) session_manager.mode = SessionMode.RUN with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/terminal/ws") as websocket: + with client.websocket_connect(TERMINAL_WS_URL) as websocket: websocket.send_text("echo hello") session_manager.mode = SessionMode.EDIT +def test_terminal_ws_unauthorized(client: TestClient) -> None: + """Test terminal websocket rejects unauthenticated connections.""" + with pytest.raises(WebSocketDisconnect) as exc_info: + with client.websocket_connect("/terminal/ws"): + pass + assert exc_info.value.code == 3000 + + +def test_terminal_ws_wrong_token(client: TestClient) -> None: + """Test terminal websocket rejects wrong token.""" + with pytest.raises(WebSocketDisconnect) as exc_info: + with client.websocket_connect("/terminal/ws?access_token=wrong-token"): + pass + assert exc_info.value.code == 3000 + + # Unit tests for terminal utility functions @@ -404,7 +422,7 @@ def test_should_close_on_command_case_variations(self) -> None: @pytest.mark.skipif(is_windows, reason="Skip on Windows") def test_terminal_ws_unicode_input(client: TestClient) -> None: """Test terminal websocket with unicode input.""" - with client.websocket_connect("/terminal/ws") as websocket: + with client.websocket_connect(TERMINAL_WS_URL) as websocket: # Send unicode command websocket.send_text("echo 'Hello 🌍'") websocket.send_text("\r") @@ -423,7 +441,7 @@ def test_terminal_ws_invalid_session_mode(client: TestClient) -> None: # Test with RUN mode session_manager.mode = SessionMode.RUN with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/terminal/ws"): + with client.websocket_connect(TERMINAL_WS_URL): pass # Should fail immediately finally:
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/marimo-team/marimo/commit/c24d4806398f30be6b12acd6c60d1d7c68cfd12anvdPatchWEB
- github.com/marimo-team/marimo/pull/9098nvdIssue TrackingPatchWEB
- github.com/marimo-team/marimo/security/advisories/GHSA-2679-6mx9-h9xcnvdExploitMitigationVendor AdvisoryWEB
- www.sysdig.com/blog/marimo-oss-python-notebook-rce-from-disclosure-to-exploitation-in-under-10-hoursnvdExploitThird Party AdvisoryWEB
- github.com/advisories/GHSA-2679-6mx9-h9xcghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-39987ghsaADVISORY
- www.cisa.gov/known-exploited-vulnerabilities-catalogghsaWEB
- www.cisa.gov/known-exploited-vulnerabilities-catalognvdUS Government ResourceWEB
News mentions
2- 13th April – Threat Intelligence ReportCheck Point Research · Apr 13, 2026
- CISA Adds One Known Exploited Vulnerability to CatalogCISA Alerts