Apr 9, 2026·5 min read·1 visit
The marimo notebook environment prior to version 0.23.0 exposes an unauthenticated terminal WebSocket endpoint (`/terminal/ws`). Attackers can connect to this endpoint to spawn a PTY shell, executing arbitrary commands with the privileges of the marimo server process.
An authentication bypass vulnerability in the marimo interactive Python notebook environment allows unauthenticated remote attackers to obtain interactive pseudo-terminal (PTY) shell access. The flaw resides in the terminal WebSocket endpoint, which fails to enforce required authentication checks, leading to critical remote code execution capabilities on the host system.
The marimo application is a reactive notebook environment for Python that provides a web-based interface backed by a local server. This server exposes several HTTP and WebSocket endpoints to facilitate interactivity, code execution, and terminal access.
The vulnerability is located within the terminal-specific WebSocket endpoint at the /terminal/ws route. In default configurations, marimo restricts access to its interactive features using an authentication token system. The server relies on a specific validation function to enforce these checks across its various communication channels.
While the primary application WebSocket route correctly implements authentication validation, the terminal endpoint omits this critical step. It strictly verifies whether the application operates in "edit mode" and confirms underlying operating system support for pseudo-terminals (PTYs). Because the endpoint fails to establish the identity of the incoming connection, any client capable of reaching the open port can initiate a connection and gain complete access to the resulting terminal session.
The underlying technical flaw is an instance of CWE-306: Missing Authentication for Critical Function. The marimo backend utilizes the Starlette asynchronous framework for HTTP routing and middleware execution. In this architecture, an AuthenticationMiddleware component is responsible for parsing inbound requests and populating user identity information.
However, Starlette's middleware design dictates that WebSocket connections are not automatically terminated or rejected upon missing authentication tokens unless explicitly programmed to do so within the specific endpoint handler. The marimo application standardizes this process through a validate_auth() utility function, which inspects the WebSocket connection for a valid access_token query parameter or corresponding session cookie.
In marimo/_server/api/endpoints/terminal.py, the websocket_endpoint handler dictates the connection lifecycle for the terminal interface. The implementation checks the application mode (app_state.mode != SessionMode.EDIT) and evaluates PTY support but completely lacks the required validate_auth(websocket) invocation. Consequently, the handler bridges the inbound connection directly to the PTY subsystem without verifying the source's authorization state.
An analysis of the source code reveals the direct omission of the validation logic. The vulnerable implementation accepts the WebSocket object and immediately instantiates the AppState without authenticating the client session.
# Vulnerable implementation prior to 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(code=1008, reason="Terminal only available in edit mode")
return
# Execution proceeds to spawn the PTYThe fix, introduced in commit c24d4806398f30be6b12acd6c60d1d7c68cfd12a, rectifies this by explicitly invoking the missing authorization check before processing any connection state. If the authentication logic fails, the connection is safely terminated with an appropriate WebSocket status code.
# Patched implementation in 0.23.0
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> None:
if not validate_auth(websocket):
await websocket.close(code=3000, reason="MARIMO_UNAUTHORIZED")
return
app_state = AppState(websocket)
if app_state.mode != SessionMode.EDIT:
await websocket.close(code=1008, reason="Terminal only available in edit mode")
returnThe following diagram illustrates the vulnerable connection flow prior to the patch application.
Exploiting this vulnerability requires network access to the target marimo server port, which defaults to 2718. The attacker begins by scanning for exposed instances or specifically targeting known deployment addresses.
Once a target is identified, the attacker crafts a standard WebSocket upgrade request targeting the /terminal/ws endpoint. The client intentionally omits any access_token query parameters or authentication cookies. The server receives the connection, processes the HTTP-to-WebSocket upgrade, and transitions into the handler logic.
The server confirms the SessionMode is set to edit, which is the typical operational state for an active notebook. It subsequently issues a pty.fork() system call, creating a new child process running the system's default shell executable. Standard input, output, and error file descriptors of the child process are routed to an asynchronous I/O loop that frames terminal data into WebSocket messages.
The attacker receives an interactive shell session. By transmitting appropriately framed WebSocket text messages containing shell commands, the attacker achieves arbitrary command execution on the target operating system.
This vulnerability achieves a CVSS v3.1 base score of 9.5 (Critical) due to the simplicity of the attack vector and the severity of the consequences. The vector evaluates to CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H.
The unauthenticated remote code execution occurs in the context of the user running the marimo server process. If marimo is executed by a standard user account on a developer workstation, the attacker gains full access to user files, environment variables, internal network resources, and adjacent credentials.
If the application is deployed in a server or containerized environment with elevated privileges, the compromise depth increases significantly. The absence of complex exploitation prerequisites renders this vulnerability highly reliable to exploit via automated tooling.
The primary remediation for this vulnerability is immediately upgrading the marimo Python package to version 0.23.0 or later. The patch completely prevents unauthenticated connections from reaching the PTY subsystem by enforcing the standard token verification mechanism.
If immediate patching is technically infeasible, administrators must restrict network access to the marimo instance. Bind the service exclusively to local loopback addresses (127.0.0.1 or localhost) and utilize a virtual private network or SSH tunneling for remote access. Do not expose the service to zero-route addresses (0.0.0.0) on untrusted networks.
Additionally, technical analysis indicates a secondary risk related to Cross-Site WebSocket Hijacking (CSWSH). If users interact with the patched marimo server through a browser that stores active session cookies, a malicious website visited by the user could attempt to initiate a WebSocket connection. Organizations should configure marimo to validate tokens strictly through query parameters and enforce strong SameSite cookie policies.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
marimo marimo-team | < 0.23.0 | 0.23.0 |
| Attribute | Detail |
|---|---|
| Bug Class | Improper Authorization (CWE-306) |
| Attack Vector | Network (WebSocket) |
| Authentication Required | None |
| Impact | Remote Code Execution (RCE) |
| CVSS v3.1 Score | 9.5 (Critical) |
| Exploit Maturity | Proof of Concept Available |
The software does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources.